Weekly reporting view (#1892)

This commit is contained in:
Kevin Papst
2020-08-16 12:20:33 +02:00
committed by GitHub
parent 2449a1e8bf
commit 2d4cfbe821
39 changed files with 1130 additions and 248 deletions

View File

@@ -42,4 +42,8 @@ class Constants
* Application wide default locale.
*/
public const DEFAULT_LOCALE = 'en';
/**
* Default color for Customer, Project and Activity entities
*/
public const DEFAULT_COLOR = '#d2d6de';
}

View File

@@ -9,7 +9,10 @@
namespace App\Controller;
use App\Configuration\LanguageFormattings;
use App\Entity\User;
use App\Timesheet\DateTimeFactory;
use App\Utils\LocaleFormats;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
use Symfony\Component\Translation\DataCollectorTranslator;
@@ -108,7 +111,22 @@ abstract class AbstractController extends BaseAbstractController implements Serv
{
return array_merge(parent::getSubscribedServices(), [
'translator' => TranslatorInterface::class,
'logger' => LoggerInterface::class
'logger' => LoggerInterface::class,
LanguageFormattings::class => LanguageFormattings::class,
]);
}
protected function getDateTimeFactory(?User $user = null): DateTimeFactory
{
if (null === $user) {
$user = $this->getUser();
}
return new DateTimeFactory(new \DateTimeZone($user->getTimezone()));
}
protected function getLocaleFormats(string $locale): LocaleFormats
{
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
}
}

View File

@@ -14,12 +14,15 @@ use App\Reporting\MonthByUser;
use App\Reporting\MonthByUserForm;
use App\Reporting\MonthlyUserList;
use App\Reporting\MonthlyUserListForm;
use App\Reporting\WeekByUser;
use App\Reporting\WeekByUserForm;
use App\Repository\Query\UserQuery;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use App\Timesheet\UserDateTimeFactory;
use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@@ -39,46 +42,79 @@ final class ReportingController extends AbstractController
* @var UserRepository
*/
private $userRepository;
/**
* @var UserDateTimeFactory
*/
private $dateTimeFactory;
public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository, UserDateTimeFactory $dateTimeFactory)
public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository)
{
$this->timesheetRepository = $timesheetRepository;
$this->userRepository = $userRepository;
$this->dateTimeFactory = $dateTimeFactory;
}
/**
* @Route(path="/", name="reporting", methods={"GET"})
* @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"})
*
* @return Response
*/
public function monthByUser(Request $request)
public function defaultReport(): Response
{
$user = $this->getUser();
return $this->redirectToRoute('report_user_week');
}
private function canSelectUser(): bool
{
if (!$this->isGranted('view_other_timesheet')) {
return false;
}
$currentUser = $this->getUser();
if ($currentUser->canSeeAllData()) {
return true;
}
if ($currentUser->hasTeamAssignment()) {
return true;
}
return false;
}
/**
* @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"})
*
* @param Request $request
* @return Response
* @throws Exception
*/
public function monthByUser(Request $request): Response
{
$currentUser = $this->getUser();
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
$localeFormats = $this->getLocaleFormats($request->getLocale());
$canChangeUser = $this->canSelectUser();
$values = new MonthByUser();
$values->setUser($user);
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$values->setUser($currentUser);
$values->setDate($dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthByUserForm::class, $values, [
'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(),
'include_user' => $canChangeUser,
'timezone' => $dateTimeFactory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'format' => $localeFormats->getDateTypeFormat(),
]);
$form->submit($request->query->all(), false);
if ($values->getUser() === null) {
$values->setUser($user);
$values->setUser($currentUser);
}
if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) {
if ($currentUser !== $values->getUser() && !$canChangeUser) {
throw new AccessDeniedException('User is not allowed to see other users timesheet');
}
if ($values->getDate() === null) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$values->setDate($dateTimeFactory->getStartOfMonth());
}
$start = $values->getDate();
@@ -110,12 +146,82 @@ final class ReportingController extends AbstractController
}
/**
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
* @Security("is_granted('view_other_timesheet')")
* @Route(path="/week_by_user", name="report_user_week", methods={"GET","POST"})
*
* @param Request $request
* @return Response
* @throws Exception
*/
public function montlyhUsersList(Request $request)
public function weekByUser(Request $request): Response
{
$currentUser = $this->getUser();
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
$localeFormats = $this->getLocaleFormats($request->getLocale());
$canChangeUser = $this->canSelectUser();
$values = new WeekByUser();
$values->setUser($currentUser);
$values->setDate($dateTimeFactory->getStartOfWeek());
$form = $this->createForm(WeekByUserForm::class, $values, [
'include_user' => $canChangeUser,
'timezone' => $dateTimeFactory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'format' => $localeFormats->getDateTypeFormat(),
]);
$form->submit($request->query->all(), false);
if ($values->getUser() === null) {
$values->setUser($currentUser);
}
if ($currentUser !== $values->getUser() && !$canChangeUser) {
throw new AccessDeniedException('User is not allowed to see other users timesheet');
}
if ($values->getDate() === null) {
$values->setDate($dateTimeFactory->getStartOfWeek());
}
$start = $dateTimeFactory->getStartOfWeek($values->getDate());
$end = $dateTimeFactory->getEndOfWeek($values->getDate());
$selectedUser = $values->getUser();
$previous = clone $start;
$previous->modify('-1 week');
$next = clone $start;
$next->modify('+1 week');
$data = $this->timesheetRepository->getDailyStats($selectedUser, $start, $end);
$rows = $this->prepareMonthlyData($data);
return $this->render('reporting/week_by_user.html.twig', [
'form' => $form->createView(),
'days' => $data,
'rows' => $rows,
'user' => $selectedUser,
'current' => $start,
'next' => $next,
'previous' => $previous,
]);
}
/**
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
* @Security("is_granted('view_other_timesheet')")
*
* @param Request $request
* @return Response
* @throws Exception
*/
public function monthlyUsersList(Request $request): Response
{
$currentUser = $this->getUser();
$dateTimeFactory = $this->getDateTimeFactory();
$localeFormats = $this->getLocaleFormats($request->getLocale());
$query = new UserQuery();
$query->setCurrentUser($currentUser);
@@ -124,18 +230,22 @@ final class ReportingController extends AbstractController
$rows = [];
$values = new MonthlyUserList();
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$values->setDate($dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthlyUserListForm::class, $values, []);
$form = $this->createForm(MonthlyUserListForm::class, $values, [
'timezone' => $dateTimeFactory->getTimezone()->getName(),
'start_date' => $values->getDate(),
'format' => $localeFormats->getDateTypeFormat(),
]);
$form->submit($request->query->all(), false);
if ($form->isSubmitted() && !$form->isValid()) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$values->setDate($dateTimeFactory->getStartOfMonth());
}
if ($values->getDate() === null) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$values->setDate($dateTimeFactory->getStartOfMonth());
}
$start = $values->getDate();

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use App\Constants;
use App\Export\Annotation as Exporter;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
@@ -36,6 +37,10 @@ trait ColorTrait
*/
public function getColor(): ?string
{
if ($this->color === Constants::DEFAULT_COLOR) {
return null;
}
return $this->color;
}

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Constants;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
@@ -17,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ColorPickerType extends AbstractType implements DataTransformerInterface
{
public const DEFAULT_COLOR = '#d2d6de';
public const DEFAULT_COLOR = Constants::DEFAULT_COLOR;
/**
* {@inheritdoc}

View File

@@ -9,8 +9,6 @@
namespace App\Form\Type;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
@@ -25,38 +23,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class MonthPickerType extends AbstractType
{
/**
* @var LocaleSettings
*/
private $localeSettings;
/**
* @var UserDateTimeFactory
*/
private $dateTime;
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
{
$this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([
'widget' => 'single_text',
'html5' => false,
'format' => $dateFormat,
'format_picker' => $pickerFormat,
'model_timezone' => $timezone,
'view_timezone' => $timezone,
'format' => DateType::HTML5_FORMAT,
'start_date' => new \DateTime(),
]);
}
@@ -66,13 +42,13 @@ final class MonthPickerType extends AbstractType
$date = $form->getData();
if (null === $date) {
$date = $this->dateTime->getStartOfMonth();
$date = $options['start_date'];
}
$view->vars['month'] = $date;
$view->vars['previousMonth'] = (clone $date)->modify('-1 month');
$view->vars['nextMonth'] = (clone $date)->modify('+1 month');
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($this->localeSettings->getDateTypeFormat());
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']);
}
/**

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a week via picker and select previous and next week.
*
* Always falls back to the current week if none or an invalid date is given.
*/
final class WeekPickerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'widget' => 'single_text',
'html5' => false,
'format' => DateType::HTML5_FORMAT,
'start_date' => new \DateTime(),
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var \DateTime|null $date */
$date = $form->getData();
if (null === $date) {
$date = $options['start_date'];
}
$view->vars['week'] = $date;
$view->vars['previousWeek'] = (clone $date)->modify('-1 week');
$view->vars['nextWeek'] = (clone $date)->modify('+1 week');
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'weekpicker';
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting;
use App\Entity\User;
abstract class DateByUser
{
/**
* @var User
*/
private $user;
/**
* @var \DateTime
*/
private $date;
public function getUser(): ?User
{
return $this->user;
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getDate(): ?\DateTime
{
return $this->date;
}
public function setDate(\DateTime $date): self
{
$this->date = $date;
return $this;
}
}

View File

@@ -9,40 +9,6 @@
namespace App\Reporting;
use App\Entity\User;
final class MonthByUser
final class MonthByUser extends DateByUser
{
/**
* @var User
*/
private $user;
/**
* @var \DateTime
*/
private $date;
public function getUser(): ?User
{
return $this->user;
}
public function setUser(User $user): MonthByUser
{
$this->user = $user;
return $this;
}
public function getDate(): ?\DateTime
{
return $this->date;
}
public function setDate(\DateTime $date): MonthByUser
{
$this->date = $date;
return $this;
}
}

View File

@@ -12,6 +12,7 @@ namespace App\Reporting;
use App\Form\Type\MonthPickerType;
use App\Form\Type\UserType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -32,7 +33,12 @@ class MonthByUserForm extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('date', MonthPickerType::class);
$builder->add('date', MonthPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'format' => $options['format'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, ['width' => false]);
@@ -46,6 +52,9 @@ class MonthByUserForm extends AbstractType
{
$resolver->setDefaults([
'data_class' => MonthByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'format' => DateType::HTML5_FORMAT,
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',

View File

@@ -11,6 +11,7 @@ namespace App\Reporting;
use App\Form\Type\MonthPickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -31,7 +32,12 @@ class MonthlyUserListForm extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('date', MonthPickerType::class);
$builder->add('date', MonthPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'format' => $options['format'],
]);
}
/**
@@ -41,6 +47,9 @@ class MonthlyUserListForm extends AbstractType
{
$resolver->setDefaults([
'data_class' => MonthlyUserList::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'format' => DateType::HTML5_FORMAT,
'csrf_protection' => false,
'method' => 'GET',
]);

View File

@@ -0,0 +1,14 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting;
final class WeekByUser extends DateByUser
{
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting;
use App\Form\Type\UserType;
use App\Form\Type\WeekPickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class WeekByUserForm extends AbstractType
{
/**
* Simplify cross linking between pages by removing the block prefix.
*
* @return null|string
*/
public function getBlockPrefix()
{
return null;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('date', WeekPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
'format' => $options['format'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, ['width' => false]);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => WeekByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'format' => DateType::HTML5_FORMAT,
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]);
}
}

View File

@@ -444,7 +444,11 @@ class TimesheetRepository extends EntityRepository
$results[$dateKey]['rate'] += $rate;
$results[$dateKey]['duration'] += $duration;
$detailsId = $result->getProject()->getCustomer()->getId() . '_' . $result->getProject()->getId();
$detailsId =
$result->getProject()->getCustomer()->getId()
. '_' . $result->getProject()->getId()
. '_' . $result->getActivity()->getId()
;
if (!isset($results[$dateKey]['details'][$detailsId])) {
$results[$dateKey]['details'][$detailsId] = [
'project' => $result->getProject(),

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Timesheet;
use DateTime;
use DateTimeZone;
class DateTimeFactory
{
/**
* @var DateTimeZone
*/
private $timezone;
public function __construct(?DateTimeZone $timezone = null)
{
if (null === $timezone) {
$timezone = new \DateTimeZone(date_default_timezone_get());
}
$this->setTimezone($timezone);
}
public function setTimezone(DateTimeZone $timezone)
{
$this->timezone = $timezone;
}
public function getTimezone(): DateTimeZone
{
return $this->timezone;
}
public function getStartOfMonth(): DateTime
{
$date = $this->createDateTime('first day of this month');
$date->setTime(0, 0, 0);
return $date;
}
public function getStartOfWeek(?DateTime $date = null): DateTime
{
if (null === $date) {
$date = $this->createDateTime('now');
}
return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 1, 0, 0, 0);
}
public function getEndOfWeek(?DateTime $date = null): DateTime
{
if (null === $date) {
$date = $this->createDateTime('now');
}
return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 7, 23, 59, 59);
}
public function getEndOfMonth(): DateTime
{
$date = $this->createDateTime('last day of this month');
$date->setTime(23, 59, 59);
return $date;
}
private function createWeekDateTime($year, $week, $day, $hour, $minute, $second)
{
$date = new DateTime('now', $this->getTimezone());
$date->setISODate($year, $week, $day);
$date->setTime($hour, $minute, $second);
return $date;
}
public function createDateTime(string $datetime = 'now'): DateTime
{
$date = new DateTime($datetime, $this->getTimezone());
return $date;
}
/**
* @param string $format
* @param null|string $datetime
* @return bool|DateTime
*/
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
{
$date = DateTime::createFromFormat($format, $datetime, $this->getTimezone());
return $date;
}
}

View File

@@ -11,26 +11,31 @@ namespace App\Timesheet;
use App\Entity\User;
use App\Security\CurrentUser;
use DateTimeZone;
class UserDateTimeFactory
/**
* @internal use DateTimeFactory instead: this one relies on the global context and will be deprecated in the future
*/
class UserDateTimeFactory extends DateTimeFactory
{
/**
* @var \DateTimeZone
*/
private $timezone;
/**
* @var CurrentUser
*/
private $user;
/**
* @var bool
*/
private $initializedFromUser = false;
public function __construct(CurrentUser $user)
{
parent::__construct(null);
$this->user = $user;
}
public function getTimezone(): \DateTimeZone
public function getTimezone(): DateTimeZone
{
if (null === $this->timezone) {
if ($this->initializedFromUser === false) {
$timezone = date_default_timezone_get();
$user = $this->user->getUser();
@@ -38,44 +43,12 @@ class UserDateTimeFactory
$timezone = $user->getTimezone();
}
$this->timezone = new \DateTimeZone($timezone);
$timezone = new DateTimeZone($timezone);
parent::setTimezone($timezone);
$this->initializedFromUser = true;
}
return $this->timezone;
}
public function getStartOfMonth(): \DateTime
{
$date = $this->createDateTime('first day of this month');
$date->setTime(0, 0, 0);
return $date;
}
public function getEndOfMonth(): \DateTime
{
$date = $this->createDateTime('last day of this month');
$date->setTime(23, 59, 59);
return $date;
}
public function createDateTime(string $datetime = 'now'): \DateTime
{
$date = new \DateTime($datetime, $this->getTimezone());
return $date;
}
/**
* @param string $format
* @param null|string $datetime
* @return bool|\DateTime
*/
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
{
$date = \DateTime::createFromFormat($format, $datetime, $this->getTimezone());
return $date;
return parent::getTimezone();
}
}

View File

@@ -230,34 +230,36 @@ class DateExtensions extends AbstractExtension
return $date->format($this->timeFormat);
}
public function monthName(\DateTime $dateTime): string
/**
* @see https://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats
* @see http://userguide.icu-project.org/formatparse/datetime
*
* @param DateTime $dateTime
* @param string $format
* @return string
*/
private function formatIntl(\DateTime $dateTime, string $format): string
{
// @see http://userguide.icu-project.org/formatparse/datetime
$formatter = new \IntlDateFormatter(
$this->locale,
\IntlDateFormatter::FULL,
\IntlDateFormatter::FULL,
$dateTime->getTimezone()->getName(),
\IntlDateFormatter::GREGORIAN,
'LLLL'
$format
);
return $formatter->format($dateTime);
}
public function monthName(\DateTime $dateTime, bool $withYear = false): string
{
return $this->formatIntl($dateTime, ($withYear ? 'LLLL yyyy' : 'LLLL'));
}
public function dayName(\DateTime $dateTime, bool $short = false): string
{
// @see http://userguide.icu-project.org/formatparse/datetime
$formatter = new \IntlDateFormatter(
$this->locale,
\IntlDateFormatter::FULL,
\IntlDateFormatter::FULL,
$dateTime->getTimezone()->getName(),
\IntlDateFormatter::GREGORIAN,
$short ? 'EE' : 'EEEE'
);
return $formatter->format($dateTime);
return $this->formatIntl($dateTime, ($short ? 'EE' : 'EEEE'));
}
/**

View File

@@ -10,6 +10,10 @@
namespace App\Twig;
use App\Constants;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\EntityWithMetaFields;
use App\Entity\Project;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -27,6 +31,7 @@ class Extensions extends AbstractExtension
return [
new TwigFilter('docu_link', [$this, 'documentationLink']),
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
new TwigFilter('color', [$this, 'color']),
];
}
@@ -40,6 +45,34 @@ class Extensions extends AbstractExtension
];
}
public function color(EntityWithMetaFields $entity): ?string
{
if ($entity instanceof Activity) {
if (!empty($entity->getColor())) {
return $entity->getColor();
}
if (null !== $entity->getProject()) {
$entity = $entity->getProject();
}
}
if ($entity instanceof Project) {
if (!empty($entity->getColor())) {
return $entity->getColor();
}
$entity = $entity->getCustomer();
}
if ($entity instanceof Customer) {
if (!empty($entity->getColor())) {
return $entity->getColor();
}
}
return null;
}
/**
* @param object $object
* @return null|string

View File

@@ -54,6 +54,7 @@ final class ReportingExtension extends AbstractExtension
$event = new ReportingEvent($user);
if ($this->security->isGranted('view_reporting')) {
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week'));
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month'));
if ($this->security->isGranted('view_other_timesheet')) {
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users'));

View File

@@ -48,26 +48,30 @@
{% block monthpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{previousMonth|date_short}}').change()">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}">
<i class="{{ 'left'|icon }}"></i>
</a>
<a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">{{ month|month_name|trans ~ ' ' ~ month|date_format('Y') }}</span>
<span id="{{ form.vars.id }}_month_name">{{ month|month_name(true) }}</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{nextMonth|date_short}}').change()">
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextMonth|month_name(true) }}">
<i class="{{ 'right'|icon }}"></i>
</a>
</div>
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
$('#{{ form.vars.id }}').on('change', function(ev) {
var newDate = moment($(this).val(), '{{ momentFormat }}').format('MMMM YYYY');
$('#{{ form.vars.id }}_month_name').html(
newDate[0].toUpperCase() + newDate.slice(1)
);
});
});
</script>
{% set type = 'hidden' %}
{{ block('form_widget_simple') }}
{{ block('hidden_widget') }}
{%- endblock monthpicker_widget %}
{% block weekpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}">
<i class="{{ 'left'|icon }}"></i>
</a>
<a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_week_number">{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}">
<i class="{{ 'right'|icon }}"></i>
</a>
</div>
{{ block('hidden_widget') }}
{%- endblock weekpicker_widget %}

View File

@@ -4,6 +4,13 @@
{% block report %}
{% set hasData = false %}
{% for day in days %}
{% if day.details is not empty %}
{% set hasData = true %}
{% endif %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_before %}
@@ -20,10 +27,14 @@
{% endif %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}user-month-reporting-box no-padding table-responsive{% endblock %}
{% block box_body_class %}user-month-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %}
{% block box_body %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th></th>
<th></th>
{% for day in days %}
<th class="text-center text-nowrap{% if day.day is weekend %} weekend{% endif %}">
@@ -31,52 +42,54 @@
{{ day.day|date_format('d.m') }}
</th>
{% endfor %}
<th></th>
</tr>
{% for project in rows %}
<tr>
<td class="text-nowrap">
<strong>{{ widgets.label_project(project.project) }}</strong>
</td>
<th class="text-nowrap text-center">{{ project.duration|duration }}</th>
{% for day in project.days %}
<td class="text-nowrap{% if day.date is weekend %} weekend{% endif %}">
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{{ day.duration|duration }}
<strong>{{ day.duration|duration }}</strong>
{% endif %}
</td>
{% endfor %}
<th class="text-nowrap">{{ project.duration|duration }}</th>
</tr>
{% for activity in project.activities %}
<tr>
<td class="text-nowrap">
{{ widgets.label_activity(activity.activity) }}
</td>
<th class="text-nowrap text-center">{{ activity.duration|duration }}</th>
{% for day in activity.days %}
<td class="text-nowrap{% if day.date is weekend %} weekend{% endif %}">
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{{ day.duration|duration }}
{% endif %}
</td>
{% endfor %}
<th class="text-nowrap">{{ activity.duration|duration }}</th>
</tr>
{% endfor %}
{% endfor %}
{% set total = 0 %}
<tr>
<th></th>
{% for day in days %}
<th class="text-nowrap{% if day.day is weekend %} weekend{% endif %}">
{% set total = total + day.totalDuration %}
{% endfor %}
<th></th>
<th class="text-nowrap text-center">{{ total|duration }}</th>
{% for day in days %}
<th class="text-nowrap text-center{% if day.day is weekend %} weekend{% endif %}">
{% if day.totalDuration > 0 %}
{{ day.totalDuration|duration }}
{% set total = total + day.totalDuration %}
{% endif %}
</th>
{% endfor %}
<th class="text-nowrap">{{ total|duration }}</th>
</tr>
</table>
{% endif %}
{% endblock %}
{% endembed %}

View File

@@ -15,10 +15,11 @@
{% block box_title %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}monthly-user-list-reporting-box no-padding table-responsive{% endblock %}
{% block box_body_class %}monthly-user-list-reporting-box table-responsive no-padding{% endblock %}
{% block box_body %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th></th>
<th></th>
{% for day in days %}
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
@@ -26,7 +27,6 @@
{{ day|date_format('d.m') }}
</th>
{% endfor %}
<th></th>
</tr>
{% for userDay in rows %}
{% set usersMonthDuration = 0 %}
@@ -34,15 +34,25 @@
<td class="text-nowrap">
<strong>{{ widgets.username(userDay.user) }}</strong>
</td>
{% for day in userDay.days %}
{% if day.totalDuration > 0 %}
{% set usersMonthDuration = usersMonthDuration + day.totalDuration %}
{% endif %}
{% endfor %}
<th class="text-nowrap text-center">
{% if usersMonthDuration == 0 %}
-
{% else %}
{{ usersMonthDuration|duration }}
{% endif %}
</th>
{% for day in userDay.days %}
<td class="text-nowrap{% if day.day is weekend %} weekend{% endif %}">
{% if day.totalDuration > 0 %}
{{ day.totalDuration|duration }}
{% set usersMonthDuration = usersMonthDuration + day.totalDuration %}
{% endif %}
</td>
{% endfor %}
<th class="text-nowrap">{{ usersMonthDuration|duration }}</th>
</tr>
{% endfor %}
</table>

View File

@@ -0,0 +1,112 @@
{% extends 'reporting/layout.html.twig' %}
{% block report_title %}{{ 'report_user_week'|trans({}, 'reporting') }}{% endblock %}
{% block report %}
{% set hasData = false %}
{% for day in days %}
{% if day.details is not empty %}
{% set hasData = true %}
{% endif %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_before %}
{{ form_start(form, {'action': path('report_user_week'), 'attr': {'class': 'form-inline'}}) }}
{% endblock %}
{% block box_after %}
{{ form_end(form) }}
{% endblock %}
{% block box_title %}
{% if form.user is defined %}
{{ form_widget(form.user) }}
{% else %}
{{ widgets.username(user) }}
{% endif %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}user-week-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %}
{% block box_body %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th></th>
<th></th>
{% for day in days %}
<th class="text-center text-nowrap{% if day.day is weekend %} weekend{% endif %}">
{{ day.day|day_name(true) }}<br>
{{ day.day|date_format('d.m') }}
</th>
{% endfor %}
</tr>
{% for project in rows %}
<tr>
<td class="text-nowrap">
<strong>{{ widgets.label_project(project.project) }}</strong>
</td>
<th class="text-nowrap text-center">{{ project.duration|duration }}</th>
{% for day in project.days %}
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
<strong>{{ day.duration|duration }}</strong>
{% endif %}
</td>
{% endfor %}
</tr>
{% for activity in project.activities %}
<tr>
<td class="text-nowrap">
{{ widgets.label_activity(activity.activity) }}
</td>
<th class="text-nowrap text-center">{{ activity.duration|duration }}</th>
{% for day in activity.days %}
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{{ day.duration|duration }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
{% set total = 0 %}
<tr>
{% for day in days %}
{% set total = total + day.totalDuration %}
{% endfor %}
<th></th>
<th class="text-nowrap text-center">{{ total|duration }}</th>
{% for day in days %}
<th class="text-nowrap text-center{% if day.day is weekend %} weekend{% endif %}">
{% if day.totalDuration > 0 %}
{{ day.totalDuration|duration }}
{% endif %}
</th>
{% endfor %}
</tr>
</table>
{% endif %}
{% endblock %}
{% endembed %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
{% if form.user is defined %}
$('#{{ form.user.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
});
{% endif %}
$('#{{ form.date.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
});
});
</script>
{% endblock %}

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Controller;
use App\Entity\User;
use App\Tests\DataFixtures\TimesheetFixtures;
/**
* @group integration
@@ -21,6 +22,16 @@ class ReportingControllerTest extends ControllerBaseTest
$this->assertUrlIsSecured('/reporting');
}
public function testWeekByUserIsSecure()
{
$this->assertUrlIsSecured('/reporting/week_by_user');
}
public function testMonthByUserIsSecure()
{
$this->assertUrlIsSecured('/reporting/month_by_user');
}
public function testMonthlyListIsSecure()
{
$this->assertUrlIsSecured('/reporting/monthly_users_list');
@@ -31,17 +42,53 @@ class ReportingControllerTest extends ControllerBaseTest
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/monthly_users_list');
}
public function testDefaultUsersMonthReport()
protected function importReportingFixture(string $role)
{
$fixture = new TimesheetFixtures();
$fixture->setAmount(50);
$fixture->setAmountRunning(10);
$fixture->setUser($this->getUserByRole($role));
$fixture->setStartDate(new \DateTime());
$this->importFixture($fixture);
}
public function testRedirectForDefaultReportUrl()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/reporting/');
$this->importReportingFixture(User::ROLE_USER);
$this->request($client, '/reporting/');
$this->assertIsRedirect($client, $this->createUrl('/reporting/week_by_user'));
$client->followRedirect();
self::assertStringContainsString('<div class="box-body user-week-reporting-box', $client->getResponse()->getContent());
}
public function testUserWeekReport()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->importReportingFixture(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/reporting/week_by_user?user=4&date=12999119191');
self::assertStringContainsString('<div class="box-body user-week-reporting-box', $client->getResponse()->getContent());
$option = $client->getCrawler()->filterXPath("//select[@id='user']/option[@selected]");
self::assertEquals(4, $option->attr('value'));
}
public function testUserMonthReport()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importReportingFixture(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/reporting/month_by_user?user=4&date=12999119191');
self::assertStringContainsString('<div class="box-body user-month-reporting-box', $client->getResponse()->getContent());
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
self::assertEquals(0, $select->count());
}
public function testMonthlyUsersReport()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importReportingFixture(User::ROLE_TEAMLEAD);
$this->assertAccessIsGranted($client, '/reporting/monthly_users_list');
self::assertStringContainsString('<div class="box-body monthly-user-list-reporting-box', $client->getResponse()->getContent());
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
self::assertEquals(0, $select->count());
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Entity;
use App\Constants;
use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\Project;
@@ -58,6 +59,9 @@ class ActivityTest extends TestCase
$this->assertInstanceOf(Activity::class, $sut->setColor('#fffccc'));
$this->assertEquals('#fffccc', $sut->getColor());
$this->assertInstanceOf(Activity::class, $sut->setColor(Constants::DEFAULT_COLOR));
$this->assertNull($sut->getColor());
$this->assertInstanceOf(Activity::class, $sut->setBudget(12345.67));
$this->assertEquals(12345.67, $sut->getBudget());

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Entity;
use App\Constants;
use App\Entity\Customer;
use App\Entity\CustomerMeta;
use App\Entity\Team;
@@ -72,6 +73,9 @@ class CustomerTest extends TestCase
self::assertInstanceOf(Customer::class, $sut->setColor('#fffccc'));
self::assertEquals('#fffccc', $sut->getColor());
self::assertInstanceOf(Customer::class, $sut->setColor(Constants::DEFAULT_COLOR));
self::assertNull($sut->getColor());
self::assertInstanceOf(Customer::class, $sut->setCompany('test company'));
self::assertEquals('test company', $sut->getCompany());

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Entity;
use App\Constants;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\ProjectMeta;
@@ -82,6 +83,9 @@ class ProjectTest extends TestCase
self::assertInstanceOf(Project::class, $sut->setColor('#fffccc'));
self::assertEquals('#fffccc', $sut->getColor());
self::assertInstanceOf(Project::class, $sut->setColor(Constants::DEFAULT_COLOR));
self::assertNull($sut->getColor());
self::assertInstanceOf(Project::class, $sut->setVisible(false));
self::assertFalse($sut->isVisible());

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Reporting;
use App\Entity\User;
use App\Reporting\DateByUser;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Reporting\DateByUser
*/
abstract class AbstractDateByUserTest extends TestCase
{
abstract protected function createSut(): DateByUser;
public function testEmptyObject()
{
$sut = $this->createSut();
self::assertNull($sut->getDate());
self::assertNull($sut->getUser());
}
public function testSetter()
{
$date = new \DateTime('2019-05-27');
$user = new User();
$user->setAlias('sdfsdfdsdf');
$sut = $this->createSut();
self::assertInstanceOf(DateByUser::class, $sut->setDate($date));
self::assertInstanceOf(DateByUser::class, $sut->setUser($user));
self::assertSame($date, $sut->getDate());
self::assertSame($user, $sut->getUser());
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Reporting;
use App\Reporting\DateByUser;
use App\Reporting\MonthByUser;
/**
* @covers \App\Reporting\MonthByUser
* @covers \App\Reporting\DateByUser
*/
class MonthByUserTest extends AbstractDateByUserTest
{
protected function createSut(): DateByUser
{
return new MonthByUser();
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Reporting;
use App\Reporting\DateByUser;
use App\Reporting\WeekByUser;
/**
* @covers \App\Reporting\WeekByUser
* @covers \App\Reporting\DateByUser
*/
class WeekByUserTest extends AbstractDateByUserTest
{
protected function createSut(): DateByUser
{
return new WeekByUser();
}
}

View File

@@ -0,0 +1,158 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Timesheet;
use App\Timesheet\DateTimeFactory;
use DateTime;
use DateTimeZone;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Timesheet\DateTimeFactory
*/
class DateTimeFactoryTest extends TestCase
{
public const TEST_TIMEZONE = 'Europe/London';
protected function createDateTimeFactory(?string $timezone = null): DateTimeFactory
{
if (null === $timezone) {
return new DateTimeFactory();
}
return new DateTimeFactory(new DateTimeZone($timezone));
}
public function testGetTimezone()
{
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName());
}
public function testGetTimezoneWithFallbackTimezone()
{
$sut = $this->createDateTimeFactory();
$this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName());
}
public function testGetStartOfMonth()
{
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getStartOfMonth();
$this->assertEquals(0, $dateTime->format('H'));
$this->assertEquals(0, $dateTime->format('i'));
$this->assertEquals(0, $dateTime->format('s'));
$this->assertEquals(1, $dateTime->format('d'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testGetEndOfMonth()
{
$expected = new DateTime('last day of this month', new DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getEndOfMonth();
$this->assertEquals(23, $dateTime->format('H'));
$this->assertEquals(59, $dateTime->format('i'));
$this->assertEquals(59, $dateTime->format('s'));
$this->assertEquals($expected->format('d'), $dateTime->format('d'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testGetStartOfWeek()
{
$expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getStartOfWeek($expected);
$this->assertEquals(0, $dateTime->format('H'));
$this->assertEquals(0, $dateTime->format('i'));
$this->assertEquals(0, $dateTime->format('s'));
$this->assertEquals(23, $dateTime->format('d'));
$this->assertEquals(1, $dateTime->format('N'));
$this->assertEquals('Monday', $dateTime->format('l'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
$dateTime = $sut->getStartOfWeek();
$this->assertEquals(0, $dateTime->format('H'));
$this->assertEquals(0, $dateTime->format('i'));
$this->assertEquals(0, $dateTime->format('s'));
$this->assertEquals(1, $dateTime->format('N'));
$this->assertEquals('Monday', $dateTime->format('l'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testGetEndOfWeek()
{
$expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getEndOfWeek($expected);
$this->assertEquals(23, $dateTime->format('H'));
$this->assertEquals(59, $dateTime->format('i'));
$this->assertEquals(59, $dateTime->format('s'));
$this->assertEquals(29, $dateTime->format('d'));
$this->assertEquals(7, $dateTime->format('N'));
$this->assertEquals('Sunday', $dateTime->format('l'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
$dateTime = $sut->getEndOfWeek();
$this->assertEquals(23, $dateTime->format('H'));
$this->assertEquals(59, $dateTime->format('i'));
$this->assertEquals(59, $dateTime->format('s'));
$this->assertEquals(7, $dateTime->format('N'));
$this->assertEquals('Sunday', $dateTime->format('l'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testCreateDateTime()
{
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->createDateTime('2015-07-24 13:45:21');
$this->assertEquals(13, $dateTime->format('H'));
$this->assertEquals(45, $dateTime->format('i'));
$this->assertEquals(21, $dateTime->format('s'));
$this->assertEquals('24', $dateTime->format('d'));
$this->assertEquals('07', $dateTime->format('m'));
$this->assertEquals('2015', $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testCreateDateTimeWithDefaultValue()
{
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->createDateTime();
$difference = $expected->getTimestamp() - $dateTime->getTimestamp();
// poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects
$this->assertTrue(2 >= $difference);
}
}

View File

@@ -14,80 +14,27 @@ use App\Timesheet\UserDateTimeFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Timesheet\DateTimeFactory
* @covers \App\Timesheet\UserDateTimeFactory
*/
class UserDateTimeFactoryTest extends TestCase
{
public const TEST_TIMEZONE = 'Europe/London';
public const TEST_TIMEZONE = 'Africa/Asmara';
protected function createDateTimeFactory(?string $timezone = null): UserDateTimeFactory
protected function createUserDateTimeFactory(?string $timezone = null): UserDateTimeFactory
{
return (new UserDateTimeFactoryFactory($this))->create($timezone);
}
public function testGetTimezone()
{
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$sut = $this->createUserDateTimeFactory(self::TEST_TIMEZONE);
$this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName());
}
public function testGetTimezoneWithFallbackTimezone()
{
$sut = $this->createDateTimeFactory();
$sut = $this->createUserDateTimeFactory();
$this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName());
}
public function testGetStartOfMonth()
{
$expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getStartOfMonth();
$this->assertEquals(0, $dateTime->format('H'));
$this->assertEquals(0, $dateTime->format('i'));
$this->assertEquals(0, $dateTime->format('s'));
$this->assertEquals(1, $dateTime->format('d'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testGetEndOfMonth()
{
$expected = new \DateTime('last day of this month', new \DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->getEndOfMonth();
$this->assertEquals(23, $dateTime->format('H'));
$this->assertEquals(59, $dateTime->format('i'));
$this->assertEquals(59, $dateTime->format('s'));
$this->assertEquals($expected->format('d'), $dateTime->format('d'));
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testCreateDateTime()
{
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->createDateTime('2015-07-24 13:45:21');
$this->assertEquals(13, $dateTime->format('H'));
$this->assertEquals(45, $dateTime->format('i'));
$this->assertEquals(21, $dateTime->format('s'));
$this->assertEquals('24', $dateTime->format('d'));
$this->assertEquals('07', $dateTime->format('m'));
$this->assertEquals('2015', $dateTime->format('Y'));
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
}
public function testCreateDateTimeWithDefaultValue()
{
$expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE));
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
$dateTime = $sut->createDateTime();
$difference = $expected->getTimestamp() - $dateTime->getTimestamp();
// poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects
$this->assertTrue(2 >= $difference);
}
}

View File

@@ -139,20 +139,27 @@ class DateExtensionsTest extends TestCase
/**
* @dataProvider getMonthNameTestData
*/
public function testMonthName(string $locale, string $date, string $expectedName)
public function testMonthName(string $locale, string $date, string $expectedName, bool $withYear = false)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date)));
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date), $withYear));
}
public function getMonthNameTestData()
{
return [
['de', '2020-07-09 23:59:59', 'Juli'],
['en', '2020-07-09 23:59:59', 'July'],
['de', 'January 2016', 'Januar'],
['en', 'January 2016', 'January'],
['en', '2016-12-23', 'December'],
['de', '2020-07-09 23:59:59', 'Juli', false],
['en', '2020-07-09 23:59:59', 'July', false],
['de', 'January 2016', 'Januar', false],
['en', 'January 2016', 'January', false],
['en', '2016-12-23', 'December', false],
['ru', '2016-12-23', 'декабрь', false],
['de', '2020-07-09 23:59:59', 'Juli 2020', true],
['en', '2020-07-09 23:59:59', 'July 2020', true],
['de', 'January 2016', 'Januar 2016', true],
['en', 'January 2016', 'January 2016', true],
['en', '2015-12-23', 'December 2015', true],
['ru', '2015-12-23', 'декабрь 2015', true],
];
}

View File

@@ -9,6 +9,9 @@
namespace App\Tests\Twig;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Twig\Extensions;
use PHPUnit\Framework\TestCase;
@@ -27,7 +30,7 @@ class ExtensionsTest extends TestCase
public function testGetFilters()
{
$filters = ['docu_link', 'multiline_indent'];
$filters = ['docu_link', 'multiline_indent', 'color'];
$sut = $this->getSut();
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
@@ -116,4 +119,39 @@ sdfsdf' . PHP_EOL . "\n" .
$sut = $this->getSut();
self::assertEquals(implode("\n", $expected), $sut->multilineIndent($string, $indent));
}
public function testColor()
{
$sut = $this->getSut();
$globalActivity = new Activity();
self::assertNull($sut->color($globalActivity));
$globalActivity->setColor('#000001');
self::assertEquals('#000001', $sut->color($globalActivity));
$customer = new Customer();
self::assertNull($sut->color($customer));
$customer->setColor('#000004');
self::assertEquals('#000004', $sut->color($customer));
$project = new Project();
self::assertNull($sut->color($project));
$project->setCustomer($customer);
self::assertEquals('#000004', $sut->color($project));
$project->setColor('#000003');
self::assertEquals('#000003', $sut->color($project));
$activity = new Activity();
self::assertNull($sut->color($activity));
$activity->setProject($project);
self::assertEquals('#000003', $sut->color($activity));
$activity->setColor('#000002');
self::assertEquals('#000002', $sut->color($activity));
}
}

View File

@@ -57,6 +57,6 @@ class ReportingExtensionTest extends TestCase
$sut = $this->getSut(true);
$reports = $sut->getAvailableReports(new User());
self::assertIsArray($reports);
self::assertCount(2, $reports);
self::assertCount(3, $reports);
}
}

View File

@@ -648,6 +648,10 @@
<!--
Statistics data for Dashboard & Users profile
-->
<trans-unit id="stats.workingTimeWeek">
<source>stats.workingTimeWeek</source>
<target>Kalenderweek %week%</target>
</trans-unit>
<trans-unit id="stats.durationToday">
<source>stats.durationToday</source>
<target>Prestaties vandaag</target>

View File

@@ -6,6 +6,10 @@
<source>reporting.title</source>
<target>Reporting</target>
</trans-unit>
<trans-unit id="report_user_week">
<source>report_user_week</source>
<target>Wochenansicht für einen Benutzer</target>
</trans-unit>
<trans-unit id="report_user_month">
<source>report_user_month</source>
<target>Monatsansicht für einen Benutzer</target>

View File

@@ -6,6 +6,10 @@
<source>reporting.title</source>
<target>Reporting</target>
</trans-unit>
<trans-unit id="report_user_week">
<source>report_user_week</source>
<target>Weekly view for one user</target>
</trans-unit>
<trans-unit id="report_user_month">
<source>report_user_month</source>
<target>Monthly view for one user</target>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="nl" datatype="plaintext" original="reporting.en.xlf">
<body>
<trans-unit id="reporting.title">
<source>reporting.title</source>
<target>Rapporten</target>
</trans-unit>
<trans-unit id="report_user_week">
<source>report_user_week</source>
<target>Weekoverzicht voor een user</target>
</trans-unit>
<trans-unit id="report_user_month">
<source>report_user_month</source>
<target>Maandoverzicht voor een user</target>
</trans-unit>
<trans-unit id="report_monthly_users">
<source>report_monthly_users</source>
<target>Maandoverzicht voor alle users</target>
</trans-unit>
</body>
</file>
</xliff>