From 59d2946b91718dfc222267381c2ddd4d06fafbff Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sat, 15 Jun 2019 23:04:53 +0200 Subject: [PATCH] add "duration + fixed start time" tracking mode (#859) --- config/packages/kimai.yaml | 14 +- src/API/TimesheetController.php | 39 +++- src/Configuration/TimesheetConfiguration.php | 12 +- src/Controller/CalendarController.php | 6 +- .../SystemConfigurationController.php | 4 +- .../TimesheetAbstractController.php | 111 ++++------ src/Controller/TimesheetTeamController.php | 9 +- src/DependencyInjection/Configuration.php | 3 + src/Form/TimesheetAdminEditForm.php | 10 +- src/Form/TimesheetEditForm.php | 22 +- ...sheetModeType.php => TrackingModeType.php} | 24 +- .../TrackingMode/AbstractTrackingMode.php | 101 +++++++++ src/Timesheet/TrackingMode/DefaultMode.php | 43 ++++ .../TrackingMode/DurationFixedStartMode.php | 72 ++++++ .../TrackingMode/DurationOnlyMode.php | 43 ++++ src/Timesheet/TrackingMode/PunchInOutMode.php | 50 +++++ .../TrackingMode/TrackingModeInterface.php | 72 ++++++ src/Timesheet/TrackingModeService.php | 62 ++++++ src/Timesheet/UserDateTimeFactory.php | 24 +- src/Twig/TimesheetConfigExtension.php | 48 ---- .../Constraints/TimesheetValidator.php | 17 +- templates/calendar/user.html.twig | 4 +- templates/timesheet-team/index.html.twig | 5 +- templates/timesheet/index.html.twig | 13 +- .../TimesheetConfigurationTest.php | 10 +- .../DependencyInjection/AppExtensionTest.php | 1 + .../TrackingMode/AbstractTrackingModeTest.php | 205 ++++++++++++++++++ .../TrackingMode/DefaultModeTest.php | 45 ++++ .../DurationFixedStartModeTest.php | 57 +++++ .../TrackingMode/DurationOnlyModeTest.php | 42 ++++ .../TrackingMode/PunchInOutModeTest.php | 45 ++++ tests/Timesheet/TrackingModeServiceTest.php | 71 ++++++ tests/Twig/TimesheetConfigExtensionTest.php | 59 ----- .../Constraints/TimesheetValidatorTest.php | 6 +- translations/system-configuration.de.xliff | 6 +- translations/system-configuration.en.xliff | 4 + 36 files changed, 1094 insertions(+), 265 deletions(-) rename src/Form/Type/{TimesheetModeType.php => TrackingModeType.php} (63%) create mode 100644 src/Timesheet/TrackingMode/AbstractTrackingMode.php create mode 100644 src/Timesheet/TrackingMode/DefaultMode.php create mode 100644 src/Timesheet/TrackingMode/DurationFixedStartMode.php create mode 100644 src/Timesheet/TrackingMode/DurationOnlyMode.php create mode 100644 src/Timesheet/TrackingMode/PunchInOutMode.php create mode 100644 src/Timesheet/TrackingMode/TrackingModeInterface.php create mode 100644 src/Timesheet/TrackingModeService.php delete mode 100644 src/Twig/TimesheetConfigExtension.php create mode 100644 tests/Timesheet/TrackingMode/AbstractTrackingModeTest.php create mode 100644 tests/Timesheet/TrackingMode/DefaultModeTest.php create mode 100644 tests/Timesheet/TrackingMode/DurationFixedStartModeTest.php create mode 100644 tests/Timesheet/TrackingMode/DurationOnlyModeTest.php create mode 100644 tests/Timesheet/TrackingMode/PunchInOutModeTest.php create mode 100644 tests/Timesheet/TrackingModeServiceTest.php delete mode 100644 tests/Twig/TimesheetConfigExtensionTest.php diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index 8d436c3a..6e4a9407 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -27,12 +27,18 @@ kimai: # renders timesheet descriptions with markdown markdown_content: false - # The time-tracking mode that should be used (allowed values: default, duration_only) - # - # default: display start and end time columns in timesheet view and form - # duration_only: display start time and duration, https://www.kimai.org/documentation/timesheet.html#duration-only-mode + # The time-tracking mode that should be used. + # See https://www.kimai.org/documentation/timesheet.html#tracking-modes mode: default + # The default time to pre-fill the "create timesheet" form (in some cases). + # This setting is only respected by some timetracking modes and not in all situations. + # + # Accepted formats, see + # - https://www.php.net/manual/en/datetime.formats.php + # - https://www.php.net/manual/en/datetime.formats.time.php + # default_begin: now + # Rounding rules are used to round the begin & end dates and the duration for timesheet records. # The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded. # Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 0dd4e126..8edd3a60 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -18,6 +18,8 @@ use App\Form\TimesheetEditForm; use App\Repository\Query\TimesheetQuery; use App\Repository\TagRepository; use App\Repository\TimesheetRepository; +use App\Timesheet\TrackingMode\TrackingModeInterface; +use App\Timesheet\TrackingModeService; use App\Timesheet\UserDateTimeFactory; use Doctrine\Common\Collections\ArrayCollection; use FOS\RestBundle\Controller\Annotations as Rest; @@ -62,21 +64,30 @@ class TimesheetController extends BaseApiController * @var TagRepository */ protected $tagRepository; - /** - * @param ViewHandlerInterface $viewHandler - * @param TimesheetRepository $repository - * @param UserDateTimeFactory $dateTime - * @param TimesheetConfiguration $configuration - * @param TagRepository $tagRepository + * @var TrackingModeService */ - public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, TagRepository $tagRepository) - { + protected $trackingModeService; + + public function __construct( + ViewHandlerInterface $viewHandler, + TimesheetRepository $repository, + UserDateTimeFactory $dateTime, + TimesheetConfiguration $configuration, + TagRepository $tagRepository, + TrackingModeService $trackingModeService + ) { $this->viewHandler = $viewHandler; $this->repository = $repository; $this->configuration = $configuration; $this->dateTime = $dateTime; $this->tagRepository = $tagRepository; + $this->trackingModeService = $trackingModeService; + } + + protected function getTrackingMode(): TrackingModeInterface + { + return $this->trackingModeService->getActiveMode(); } /** @@ -271,11 +282,15 @@ class TimesheetController extends BaseApiController $timesheet->setUser($this->getUser()); $timesheet->setBegin($this->dateTime->createDateTime()); + $mode = $this->getTrackingMode(); + $form = $this->createForm(TimesheetEditForm::class, $timesheet, [ 'csrf_protection' => false, 'include_rate' => $this->isGranted('edit_rate', $timesheet), 'include_exported' => $this->isGranted('edit_export', $timesheet), - 'include_datetime' => !$this->configuration->isPunchInOut(), + 'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(), + 'allow_end_datetime' => $mode->canUpdateTimesWithAPI(), + 'allow_duration' => false, 'date_format' => self::DATE_FORMAT, ]); @@ -349,11 +364,15 @@ class TimesheetController extends BaseApiController throw new AccessDeniedHttpException('You are not allowed to update this timesheet'); } + $mode = $this->getTrackingMode(); + $form = $this->createForm(TimesheetEditForm::class, $timesheet, [ 'csrf_protection' => false, 'include_rate' => $this->isGranted('edit_rate', $timesheet), 'include_exported' => $this->isGranted('edit_export', $timesheet), - 'include_datetime' => !$this->configuration->isPunchInOut(), + 'allow_begin_datetime' => $mode->canUpdateTimesWithAPI(), + 'allow_end_datetime' => $mode->canUpdateTimesWithAPI(), + 'allow_duration' => false, 'date_format' => self::DATE_FORMAT, ]); diff --git a/src/Configuration/TimesheetConfiguration.php b/src/Configuration/TimesheetConfiguration.php index 3c5db348..df6548f8 100644 --- a/src/Configuration/TimesheetConfiguration.php +++ b/src/Configuration/TimesheetConfiguration.php @@ -11,10 +11,6 @@ namespace App\Configuration; class TimesheetConfiguration implements SystemBundleConfiguration { - public const MODE_DURATION_ONLY = 'duration_only'; - public const MODE_DEFAULT = 'default'; - public const MODE_PUNCH_IN_OUT = 'punch'; - use StringAccessibleConfigTrait; public function getPrefix(): string @@ -27,14 +23,14 @@ class TimesheetConfiguration implements SystemBundleConfiguration return (bool) $this->find('rules.allow_future_times'); } - public function isDurationOnly(): bool + public function getTrackingMode(): string { - return $this->find('mode') === self::MODE_DURATION_ONLY; + return (string) $this->find('mode'); } - public function isPunchInOut(): bool + public function getDefaultBeginTime(): string { - return $this->find('mode') === self::MODE_PUNCH_IN_OUT; + return (string) $this->find('default_begin'); } public function isMarkdownEnabled(): bool diff --git a/src/Controller/CalendarController.php b/src/Controller/CalendarController.php index c9f3add6..36c14378 100644 --- a/src/Controller/CalendarController.php +++ b/src/Controller/CalendarController.php @@ -12,6 +12,7 @@ namespace App\Controller; use App\Calendar\Google; use App\Calendar\Source; use App\Configuration\CalendarConfiguration; +use App\Timesheet\TrackingModeService; use App\Timesheet\UserDateTimeFactory; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\Routing\Annotation\Route; @@ -27,12 +28,15 @@ class CalendarController extends AbstractController /** * @Route(path="/", name="calendar", methods={"GET"}) */ - public function userCalendar(CalendarConfiguration $configuration, UserDateTimeFactory $dateTime) + public function userCalendar(CalendarConfiguration $configuration, UserDateTimeFactory $dateTime, TrackingModeService $service) { + $mode = $service->getActiveMode(); + return $this->render('calendar/user.html.twig', [ 'config' => $configuration, 'google' => $this->getGoogleSources($configuration), 'now' => $dateTime->createDateTime(), + 'is_punch_mode' => !$mode->canEditDuration() && !$mode->canEditBegin() && !$mode->canEditEnd() ]); } diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php index 177ed106..ac8f7ccd 100644 --- a/src/Controller/SystemConfigurationController.php +++ b/src/Controller/SystemConfigurationController.php @@ -15,7 +15,7 @@ use App\Form\Model\Configuration; use App\Form\Model\SystemConfiguration as SystemConfigurationModel; use App\Form\SystemConfigurationForm; use App\Form\Type\EnhancedSelectboxType; -use App\Form\Type\TimesheetModeType; +use App\Form\Type\TrackingModeType; use App\Repository\ConfigurationRepository; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -189,7 +189,7 @@ class SystemConfigurationController extends AbstractController ->setConfiguration([ (new Configuration()) ->setName('timesheet.mode') - ->setType(TimesheetModeType::class) + ->setType(TrackingModeType::class) ->setTranslationDomain('system-configuration'), (new Configuration()) ->setName('timesheet.rules.allow_future_times') diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index 3897c6a3..fe07938d 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -19,6 +19,8 @@ use App\Repository\ProjectRepository; use App\Repository\Query\TimesheetQuery; use App\Repository\TagRepository; use App\Repository\TimesheetRepository; +use App\Timesheet\TrackingMode\TrackingModeInterface; +use App\Timesheet\TrackingModeService; use App\Timesheet\UserDateTimeFactory; use Doctrine\Common\Collections\ArrayCollection; use Pagerfanta\Pagerfanta; @@ -40,26 +42,34 @@ abstract class TimesheetAbstractController extends AbstractController * @var TimesheetRepository */ protected $repository; + /** + * @var TrackingModeService + */ + protected $trackingModeService; - public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, TimesheetRepository $repository) - { + public function __construct( + UserDateTimeFactory $dateTime, + TimesheetConfiguration $configuration, + TimesheetRepository $repository, + TrackingModeService $service + ) { $this->dateTime = $dateTime; $this->configuration = $configuration; $this->repository = $repository; + $this->trackingModeService = $service; } - /** - * @return int - */ - protected function getSoftLimit() + protected function getTrackingMode(): TrackingModeInterface + { + return $this->trackingModeService->getActiveMode(); + } + + protected function getSoftLimit(): int { return $this->configuration->getActiveEntriesSoftLimit(); } - /** - * @return TimesheetRepository - */ - protected function getRepository() + protected function getRepository(): TimesheetRepository { return $this->repository; } @@ -107,6 +117,7 @@ abstract class TimesheetAbstractController extends AbstractController 'showFilter' => $form->isSubmitted(), 'toolbarForm' => $form->createView(), 'showSummary' => $this->includeSummary(), + 'showStartEndTime' => $this->canSeeStartEndTime() ]); } @@ -150,8 +161,6 @@ abstract class TimesheetAbstractController extends AbstractController $entry->setUser($this->getUser()); $entry->setBegin($this->dateTime->createDateTime()); - $this->setBeginEndFromRequest($request, $entry); - if ($request->query->get('project')) { $project = $projectRepository->find($request->query->get('project')); $entry->setProject($project); @@ -162,7 +171,10 @@ abstract class TimesheetAbstractController extends AbstractController $entry->setActivity($activity); } - $createForm = $this->getCreateForm($entry); + $mode = $this->getTrackingMode(); + $mode->create($entry, $request); + + $createForm = $this->getCreateForm($entry, $mode); $createForm->handleRequest($request); if ($createForm->isSubmitted() && $createForm->isValid()) { @@ -192,52 +204,6 @@ abstract class TimesheetAbstractController extends AbstractController ]); } - protected function setBeginEndFromRequest(Request $request, Timesheet $entry) - { - if ($this->configuration->isPunchInOut()) { - return; - } - - $start = $request->get('begin'); - if ($start !== null) { - $start = $this->dateTime->createDateTimeFromFormat('Y-m-d', $start); - if ($start !== false) { - $entry->setBegin($start); - - // only check for an end date if a begin date was given - $end = $request->get('end'); - if ($end !== null) { - $end = $this->dateTime->createDateTimeFromFormat('Y-m-d', $end); - if ($end !== false) { - $start->setTime(10, 0, 0); - $end->setTime(18, 0, 0); - - $entry->setEnd($end); - $entry->setDuration($end->getTimestamp() - $start->getTimestamp()); - } - } - } - } - - $from = $request->get('from'); - if ($from !== null) { - $from = $this->dateTime->createDateTime($from); - if ($from !== false) { - $entry->setBegin($from); - - // only check for an end datetime if a begin datetime was given - $to = $request->get('to'); - if ($to !== null) { - $to = $this->dateTime->createDateTime($to); - if ($to !== false) { - $entry->setEnd($to); - $entry->setDuration($to->getTimestamp() - $from->getTimestamp()); - } - } - } - } - } - /** * @param Request $request * @param string $renderTemplate @@ -281,19 +247,16 @@ abstract class TimesheetAbstractController extends AbstractController ]); } - /** - * @param Timesheet $entry - * @return \Symfony\Component\Form\FormInterface - */ - protected function getCreateForm(Timesheet $entry) + protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface { return $this->createForm($this->getCreateFormClassName(), $entry, [ 'action' => $this->generateUrl($this->getCreateRoute()), 'include_rate' => $this->isGranted('edit_rate', $entry), 'include_exported' => $this->isGranted('edit_export', $entry), 'include_user' => $this->includeUserInForms(), - 'use_duration' => $this->configuration->isDurationOnly(), - 'include_datetime' => !$this->configuration->isPunchInOut(), + 'allow_begin_datetime' => $mode->canEditBegin(), + 'allow_end_datetime' => $mode->canEditEnd(), + 'allow_duration' => $mode->canEditDuration(), 'customer' => true, ]); } @@ -305,6 +268,8 @@ abstract class TimesheetAbstractController extends AbstractController */ protected function getEditForm(Timesheet $entry, $page) { + $mode = $this->getTrackingMode(); + return $this->createForm($this->getEditFormClassName(), $entry, [ 'action' => $this->generateUrl($this->getEditRoute(), [ 'id' => $entry->getId(), @@ -313,8 +278,9 @@ abstract class TimesheetAbstractController extends AbstractController 'include_rate' => $this->isGranted('edit_rate', $entry), 'include_exported' => $this->isGranted('edit_export', $entry), 'include_user' => $this->includeUserInForms(), - 'include_datetime' => !$this->configuration->isPunchInOut(), - 'use_duration' => $this->configuration->isDurationOnly(), + 'allow_begin_datetime' => $mode->canEditBegin(), + 'allow_end_datetime' => $mode->canEditEnd(), + 'allow_duration' => $mode->canEditDuration(), 'customer' => true, ]); } @@ -334,12 +300,12 @@ abstract class TimesheetAbstractController extends AbstractController ]); } - protected function getCreateFormClassName() + protected function getCreateFormClassName(): string { return TimesheetEditForm::class; } - protected function getEditFormClassName() + protected function getEditFormClassName(): string { return TimesheetEditForm::class; } @@ -368,4 +334,9 @@ abstract class TimesheetAbstractController extends AbstractController { return 'timesheet_create'; } + + protected function canSeeStartEndTime(): bool + { + return $this->getTrackingMode()->canSeeBeginAndEndTimes(); + } } diff --git a/src/Controller/TimesheetTeamController.php b/src/Controller/TimesheetTeamController.php index 2776edda..71d3e373 100644 --- a/src/Controller/TimesheetTeamController.php +++ b/src/Controller/TimesheetTeamController.php @@ -75,12 +75,12 @@ class TimesheetTeamController extends TimesheetAbstractController return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository); } - protected function getCreateFormClassName() + protected function getCreateFormClassName(): string { return TimesheetAdminEditForm::class; } - protected function getEditFormClassName() + protected function getEditFormClassName(): string { return TimesheetAdminEditForm::class; } @@ -104,4 +104,9 @@ class TimesheetTeamController extends TimesheetAbstractController { return 'admin_timesheet_create'; } + + protected function canSeeStartEndTime(): bool + { + return true; + } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 0dda8e13..19c970a7 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -75,6 +75,9 @@ class Configuration implements ConfigurationInterface $node ->children() + ->scalarNode('default_begin') + ->defaultValue('now') + ->end() ->booleanNode('duration_only') ->setDeprecated() ->end() diff --git a/src/Form/TimesheetAdminEditForm.php b/src/Form/TimesheetAdminEditForm.php index a2835fe0..49fccf62 100644 --- a/src/Form/TimesheetAdminEditForm.php +++ b/src/Form/TimesheetAdminEditForm.php @@ -9,11 +9,17 @@ namespace App\Form; +use Symfony\Component\Form\FormBuilderInterface; + class TimesheetAdminEditForm extends TimesheetEditForm { - protected function showTimeFields(array $options): bool + public function buildForm(FormBuilderInterface $builder, array $options) { - return true; + $options['allow_begin_datetime'] = true; + $options['allow_end_datetime'] = true; + $options['allow_duration'] = false; + + parent::buildForm($builder, $options); } protected function showCustomer(array $options, bool $isNew, int $customerCount): bool diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 5a041127..12e57e01 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -114,14 +114,14 @@ class TimesheetEditForm extends AbstractType $dateTimeOptions['format'] = $options['date_format']; } - if ($this->showTimeFields($options)) { + if ($options['allow_begin_datetime']) { $this->addBegin($builder, $dateTimeOptions); + } - if ($options['use_duration']) { - $this->addDuration($builder); - } else { - $this->addEnd($builder, $dateTimeOptions); - } + if ($options['allow_duration']) { + $this->addDuration($builder); + } elseif ($options['allow_end_datetime']) { + $this->addEnd($builder, $dateTimeOptions); } if ($this->showCustomer($options, $isNew, $customerCount)) { @@ -154,11 +154,6 @@ class TimesheetEditForm extends AbstractType return true; } - protected function showTimeFields(array $options): bool - { - return $options['include_datetime']; - } - protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null) { $builder @@ -377,8 +372,9 @@ class TimesheetEditForm extends AbstractType 'method' => 'POST', 'date_format' => null, 'customer' => false, // for API usage - 'use_duration' => false, // duration instead of end (for duration_only mode) - 'include_datetime' => true, + 'allow_begin_datetime' => true, + 'allow_end_datetime' => true, + 'allow_duration' => false, 'attr' => [ 'data-form-event' => 'kimai.timesheetUpdate', 'data-msg-success' => 'action.update.success', diff --git a/src/Form/Type/TimesheetModeType.php b/src/Form/Type/TrackingModeType.php similarity index 63% rename from src/Form/Type/TimesheetModeType.php rename to src/Form/Type/TrackingModeType.php index 748fa18b..762e3aba 100644 --- a/src/Form/Type/TimesheetModeType.php +++ b/src/Form/Type/TrackingModeType.php @@ -9,7 +9,7 @@ namespace App\Form\Type; -use App\Configuration\TimesheetConfiguration; +use App\Timesheet\TrackingModeService; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -17,20 +17,30 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** * Custom form field type to select the timesheet mode. */ -class TimesheetModeType extends AbstractType +class TrackingModeType extends AbstractType { + protected $service; + + public function __construct(TrackingModeService $service) + { + $this->service = $service; + } + /** * {@inheritdoc} */ public function configureOptions(OptionsResolver $resolver) { + $choices = []; + + foreach ($this->service->getModes() as $mode) { + $id = $mode->getId(); + $choices['label.timesheet.mode_' . $id] = $id; + } + $resolver->setDefaults([ 'label' => 'label.timesheet.mode', - 'choices' => [ - 'label.timesheet.mode_default' => TimesheetConfiguration::MODE_DEFAULT, - 'label.timesheet.mode_punch' => TimesheetConfiguration::MODE_PUNCH_IN_OUT, - 'label.timesheet.mode_duration_only' => TimesheetConfiguration::MODE_DURATION_ONLY, - ], + 'choices' => $choices, ]); } diff --git a/src/Timesheet/TrackingMode/AbstractTrackingMode.php b/src/Timesheet/TrackingMode/AbstractTrackingMode.php new file mode 100644 index 00000000..2949587f --- /dev/null +++ b/src/Timesheet/TrackingMode/AbstractTrackingMode.php @@ -0,0 +1,101 @@ +dateTime = $dateTime; + $this->configuration = $configuration; + } + + public function create(Timesheet $timesheet, Request $request): void + { + $this->setBeginEndFromRequest($timesheet, $request); + $this->setFromToFromRequest($timesheet, $request); + } + + protected function setBeginEndFromRequest(Timesheet $entry, Request $request) + { + $start = $request->get('begin'); + if (null === $start) { + return; + } + + $start = $this->dateTime->createDateTimeFromFormat('Y-m-d', $start); + if (false === $start) { + return; + } + + $entry->setBegin($start); + + // only check for an end date if a begin date was given + $end = $request->get('end'); + if (null === $end) { + return; + } + + $end = $this->dateTime->createDateTimeFromFormat('Y-m-d', $end); + if (false === $end) { + return; + } + + $start->setTime(10, 0, 0); + $end->setTime(18, 0, 0); + + $entry->setEnd($end); + $entry->setDuration($end->getTimestamp() - $start->getTimestamp()); + } + + protected function setFromToFromRequest(Timesheet $entry, Request $request) + { + $from = $request->get('from'); + if (null === $from) { + return; + } + + try { + $from = $this->dateTime->createDateTime($from); + } catch (\Exception $ex) { + return; + } + + $entry->setBegin($from); + + $to = $request->get('to'); + if (null === $to) { + return; + } + + try { + $to = $this->dateTime->createDateTime($to); + } catch (\Exception $ex) { + return; + } + + $entry->setEnd($to); + $entry->setDuration($to->getTimestamp() - $from->getTimestamp()); + } +} diff --git a/src/Timesheet/TrackingMode/DefaultMode.php b/src/Timesheet/TrackingMode/DefaultMode.php new file mode 100644 index 00000000..9a01a1fd --- /dev/null +++ b/src/Timesheet/TrackingMode/DefaultMode.php @@ -0,0 +1,43 @@ +dateTime = $dateTime; + $this->configuration = $configuration; + } + + public function canEditBegin(): bool + { + return false; + } + + public function canEditEnd(): bool + { + return false; + } + + public function canEditDuration(): bool + { + return true; + } + + public function canUpdateTimesWithAPI(): bool + { + return false; + } + + public function create(Timesheet $timesheet, Request $request): void + { + if (null === $timesheet->getBegin()) { + $timesheet->setBegin($this->dateTime->createDateTime()); + } + + $timesheet->getBegin()->modify($this->configuration->getDefaultBeginTime()); + } + + public function getId(): string + { + return 'duration_fixed_start'; + } + + public function canSeeBeginAndEndTimes(): bool + { + return false; + } +} diff --git a/src/Timesheet/TrackingMode/DurationOnlyMode.php b/src/Timesheet/TrackingMode/DurationOnlyMode.php new file mode 100644 index 00000000..1382f5d0 --- /dev/null +++ b/src/Timesheet/TrackingMode/DurationOnlyMode.php @@ -0,0 +1,43 @@ +dateTime = $dateTime; + $this->configuration = $configuration; + } + + /** + * @return TrackingModeInterface[] + */ + public function getModes(): iterable + { + return [ + new DefaultMode($this->dateTime, $this->configuration), + new PunchInOutMode(), + new DurationOnlyMode($this->dateTime, $this->configuration), + new DurationFixedStartMode($this->dateTime, $this->configuration), + ]; + } + + public function getActiveMode(): TrackingModeInterface + { + $trackingMode = $this->configuration->getTrackingMode(); + + foreach ($this->getModes() as $mode) { + if ($mode->getId() === $trackingMode) { + return $mode; + } + } + + throw new ServiceNotFoundException($trackingMode); + } +} diff --git a/src/Timesheet/UserDateTimeFactory.php b/src/Timesheet/UserDateTimeFactory.php index a99a041f..f0c8ab05 100644 --- a/src/Timesheet/UserDateTimeFactory.php +++ b/src/Timesheet/UserDateTimeFactory.php @@ -19,9 +19,6 @@ class UserDateTimeFactory */ protected $timezone; - /** - * @param CurrentUser $user - */ public function __construct(CurrentUser $user) { $timezone = date_default_timezone_get(); @@ -34,18 +31,12 @@ class UserDateTimeFactory $this->timezone = new \DateTimeZone($timezone); } - /** - * @return \DateTimeZone - */ - public function getTimezone() + public function getTimezone(): \DateTimeZone { return $this->timezone; } - /** - * @return \DateTime - */ - public function getStartOfMonth() + public function getStartOfMonth(): \DateTime { $date = $this->createDateTime('first day of this month'); $date->setTime(0, 0, 0); @@ -53,10 +44,7 @@ class UserDateTimeFactory return $date; } - /** - * @return \DateTime - */ - public function getEndOfMonth() + public function getEndOfMonth(): \DateTime { $date = $this->createDateTime('last day of this month'); $date->setTime(23, 59, 59); @@ -64,11 +52,7 @@ class UserDateTimeFactory return $date; } - /** - * @param string $datetime - * @return \DateTime - */ - public function createDateTime(string $datetime = 'now') + public function createDateTime(string $datetime = 'now'): \DateTime { $date = new \DateTime($datetime, $this->timezone); diff --git a/src/Twig/TimesheetConfigExtension.php b/src/Twig/TimesheetConfigExtension.php deleted file mode 100644 index 266e3ad4..00000000 --- a/src/Twig/TimesheetConfigExtension.php +++ /dev/null @@ -1,48 +0,0 @@ -configuration = $configuration; - } - - /** - * @return TwigFunction[] - */ - public function getFunctions() - { - return [ - new TwigFunction('is_duration_only', [$this, 'isDurationOnly']), - new TwigFunction('is_punch_mode', [$this, 'isPunchInOut']), - ]; - } - - public function isDurationOnly(): bool - { - return $this->configuration->isDurationOnly(); - } - - public function isPunchInOut(): bool - { - return $this->configuration->isPunchInOut(); - } -} diff --git a/src/Validator/Constraints/TimesheetValidator.php b/src/Validator/Constraints/TimesheetValidator.php index b8ac1898..0d059927 100644 --- a/src/Validator/Constraints/TimesheetValidator.php +++ b/src/Validator/Constraints/TimesheetValidator.php @@ -11,6 +11,7 @@ namespace App\Validator\Constraints; use App\Configuration\TimesheetConfiguration; use App\Entity\Timesheet as TimesheetEntity; +use App\Timesheet\TrackingModeService; use App\Validator\Constraints\Timesheet as TimesheetConstraint; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraint; @@ -28,15 +29,20 @@ class TimesheetValidator extends ConstraintValidator * @var TimesheetConfiguration */ protected $configuration; + /** + * @var TrackingModeService + */ + protected $trackingModeService; /** * @param AuthorizationCheckerInterface $auth * @param TimesheetConfiguration $configuration */ - public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration) + public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration, TrackingModeService $service) { $this->auth = $auth; $this->configuration = $configuration; + $this->trackingModeService = $service; } /** @@ -68,9 +74,16 @@ class TimesheetValidator extends ConstraintValidator // an entry is edited and the end date is removed (or duration deleted) would restart the record, // which might be disallowed for the current user if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) { + $mode = $this->trackingModeService->getActiveMode(); + $path = 'start'; + if ($mode->canEditEnd()) { + $path = 'end'; + } elseif ($mode->canEditDuration()) { + $path = 'duration'; + } if (!$this->auth->isGranted('start', $timesheet)) { $context->buildViolation('You are not allowed to start this timesheet record.') - ->atPath($this->configuration->isDurationOnly() ? 'duration' : 'end') + ->atPath($path) ->setTranslationDomain('validators') ->setCode(TimesheetConstraint::START_DISALLOWED) ->addViolation(); diff --git a/templates/calendar/user.html.twig b/templates/calendar/user.html.twig index 04f7e4dc..ca7cd281 100644 --- a/templates/calendar/user.html.twig +++ b/templates/calendar/user.html.twig @@ -163,7 +163,7 @@ html: true }); }, - {% if not is_punch_mode() and is_granted('create_own_timesheet') %} + {% if not is_punch_mode and is_granted('create_own_timesheet') %} dayClick: function(date, jsEvent, view) { // day-clicks are always triggered, unless a selection was created // so clicking in a day (month view) or any slot (week and day view) will trigger a dayClick @@ -197,7 +197,7 @@ var editUrl = '{{ path('timesheet_edit', {id: '-XX-'}) }}'.replace('-XX-', eventObj.id); kimai.getPlugin('modal').openUrlInModal(editUrl); }, - {% if not is_punch_mode() %} + {% if not is_punch_mode %} editable: true, eventDragStart: function(event, jsEvent, ui, view) { window.hidePopover = true; diff --git a/templates/timesheet-team/index.html.twig b/templates/timesheet-team/index.html.twig index 10f5b241..b6317ef8 100644 --- a/templates/timesheet-team/index.html.twig +++ b/templates/timesheet-team/index.html.twig @@ -5,12 +5,11 @@ {% import "macros/actions.html.twig" as actions %} {% set tableName = 'timesheet_admin' %} -{% set duration_only = is_duration_only() %} {% set columns = { 'date': 'alwaysVisible', } %} -{% if not duration_only %} +{% if showStartEndTime %} {% set columns = columns|merge({ 'starttime': 'hidden-xs', 'endtime': 'hidden-xs' @@ -49,7 +48,7 @@ {{ entry.begin|date_short }} - {% if not duration_only %} + {% if showStartEndTime %} {{ entry.begin|time }} {% if entry.end %} diff --git a/templates/timesheet/index.html.twig b/templates/timesheet/index.html.twig index b8cd70fc..ace50eca 100644 --- a/templates/timesheet/index.html.twig +++ b/templates/timesheet/index.html.twig @@ -6,13 +6,12 @@ {% import _self as timesheet %} {% set tableName = 'timesheet' %} -{% set duration_only = is_duration_only() %} {% set canSeeRate = is_granted('view_rate_own_timesheet') %} {% set columns = { 'date': 'alwaysVisible', } %} -{% if not duration_only %} +{% if showStartEndTime %} {% set columns = columns|merge({ 'starttime': '', 'endtime': 'hidden-xs' @@ -56,7 +55,7 @@ {% set day = entry.begin|date_short %} {% endif %} {%- if showSummary and day is not same as(entry.begin|date_short) -%} - {{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, duration_only, tableName) }} + {{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, showStartEndTime, tableName) }} {% set day = entry.begin|date_short %} {% set dayDuration = 0 %} {% set dayRate = {} %} @@ -64,7 +63,7 @@ {{ entry.begin|date_short }} - {% if not duration_only %} + {% if showStartEndTime %} {{ entry.begin|time }} {% if entry.end %} @@ -112,7 +111,7 @@ {% endfor %} {% if showSummary %} - {{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, duration_only, tableName) }} + {{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, showStartEndTime, tableName) }} {% endif %} {{ tables.data_table_footer(entries, 'timesheet_paginated') }} @@ -120,11 +119,11 @@ {% endblock %} -{% macro summary(day, duration, dayRates, columns, canSeeRate, duration_only, tableName) %} +{% macro summary(day, duration, dayRates, columns, canSeeRate, showStartEndTime, tableName) %} {% import "macros/datatables.html.twig" as tables %} {{ day }} - {% if not duration_only %} + {% if showStartEndTime %} {% endif %} diff --git a/tests/Configuration/TimesheetConfigurationTest.php b/tests/Configuration/TimesheetConfigurationTest.php index 3179fcbc..e16cd709 100644 --- a/tests/Configuration/TimesheetConfigurationTest.php +++ b/tests/Configuration/TimesheetConfigurationTest.php @@ -43,6 +43,7 @@ class TimesheetConfigurationTest extends TestCase 'hard_limit' => 99, 'soft_limit' => 15, ], + 'default_begin' => 'now', ]; } @@ -52,6 +53,7 @@ class TimesheetConfigurationTest extends TestCase (new Configuration())->setName('timesheet.rules.allow_future_times')->setValue('1'), (new Configuration())->setName('timesheet.mode')->setValue('default'), (new Configuration())->setName('timesheet.markdown_content')->setValue('1'), + (new Configuration())->setName('timesheet.default_begin')->setValue('07:00'), (new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'), (new Configuration())->setName('timesheet.active_entries.soft_limit')->setValue('3'), ]; @@ -69,8 +71,9 @@ class TimesheetConfigurationTest extends TestCase $this->assertEquals(99, $sut->getActiveEntriesHardLimit()); $this->assertEquals(15, $sut->getActiveEntriesSoftLimit()); $this->assertEquals(false, $sut->isAllowFutureTimes()); - $this->assertEquals(true, $sut->isDurationOnly()); $this->assertEquals(false, $sut->isMarkdownEnabled()); + $this->assertEquals('duration_only', $sut->getTrackingMode()); + $this->assertEquals('now', $sut->getDefaultBeginTime()); } public function testDefaultWithLoader() @@ -79,8 +82,9 @@ class TimesheetConfigurationTest extends TestCase $this->assertEquals(7, $sut->getActiveEntriesHardLimit()); $this->assertEquals(3, $sut->getActiveEntriesSoftLimit()); $this->assertEquals(true, $sut->isAllowFutureTimes()); - $this->assertEquals(false, $sut->isDurationOnly()); $this->assertEquals(true, $sut->isMarkdownEnabled()); + $this->assertEquals('default', $sut->getTrackingMode()); + $this->assertEquals('07:00', $sut->getDefaultBeginTime()); } public function testDefaultWithMixedConfigs() @@ -88,7 +92,7 @@ class TimesheetConfigurationTest extends TestCase $sut = $this->getSut($this->getDefaultSettings(), [ (new Configuration())->setName('timesheet.mode')->setValue('sdf'), ]); - $this->assertEquals(false, $sut->isDurationOnly()); + $this->assertEquals('sdf', $sut->getTrackingMode()); } public function testFindByKey() diff --git a/tests/DependencyInjection/AppExtensionTest.php b/tests/DependencyInjection/AppExtensionTest.php index 7eb97d35..85e2dcb3 100644 --- a/tests/DependencyInjection/AppExtensionTest.php +++ b/tests/DependencyInjection/AppExtensionTest.php @@ -129,6 +129,7 @@ class AppExtensionTest extends TestCase 'rules' => [ 'allow_future_times' => true, ], + 'default_begin' => 'now', ], 'kimai.timesheet.rates' => [], 'kimai.timesheet.rounding' => [], diff --git a/tests/Timesheet/TrackingMode/AbstractTrackingModeTest.php b/tests/Timesheet/TrackingMode/AbstractTrackingModeTest.php new file mode 100644 index 00000000..bc4fc82d --- /dev/null +++ b/tests/Timesheet/TrackingMode/AbstractTrackingModeTest.php @@ -0,0 +1,205 @@ +createSut(); + + $timesheet = new Timesheet(); + + self::assertNull($timesheet->getBegin()); + self::assertNull($timesheet->getEnd()); + + $sut->create($timesheet, new Request()); + + self::assertNull($timesheet->getBegin()); + self::assertNull($timesheet->getEnd()); + } + + public function testCreateUseBeginWithoutEndDateFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'begin' => '2017-07-23', + ]); + + $sut->create($timesheet, $request); + + self::assertEquals('2017-07-23', $timesheet->getBegin()->format('Y-m-d')); + self::assertNotEquals('10:00:00', $timesheet->getBegin()->format('H:i:s')); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } + + public function testCreateUseBeginEndDateFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'begin' => '2017-07-23', + 'end' => '2017-07-23', + ]); + + $sut->create($timesheet, $request); + + self::assertNotNull($timesheet->getBegin()); + self::assertNotNull($timesheet->getEnd()); + + self::assertEquals('2017-07-23 10:00:00', $timesheet->getBegin()->format('Y-m-d H:i:s')); + self::assertEquals('2017-07-23 18:00:00', $timesheet->getEnd()->format('Y-m-d H:i:s')); + self::assertEquals(28800, $timesheet->getDuration()); + } + + public function testCreateIgnoresValidEndOnInvalidBeginDateFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'begin' => '10x0-99-99', + 'end' => '2017-07-23', + ]); + + $sut->create($timesheet, $request); + + self::assertNull($timesheet->getBegin()); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } + + public function testCreateUsesBeginAndIgnoresInvalidEndDateFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'begin' => '2017-07-23', + 'end' => '20xx-07-23', + ]); + + $sut->create($timesheet, $request); + + self::assertEquals('2017-07-23', $timesheet->getBegin()->format('Y-m-d')); + self::assertNotEquals('10:00:00', $timesheet->getBegin()->format('H:i:s')); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } + + public function testCreateUseFromWithoutToDatetimeFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'from' => '2018-05-23 21:47:55', + ]); + + $sut->create($timesheet, $request); + + self::assertEquals('2018-05-23 21:47:55', $timesheet->getBegin()->format('Y-m-d H:i:s')); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } + + public function testCreateUseFromToDatetimeFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'from' => '2018-05-23 21:47:55', + 'to' => '2018-05-24 01:11:11', + ]); + + $sut->create($timesheet, $request); + + self::assertNotNull($timesheet->getBegin()); + self::assertNotNull($timesheet->getEnd()); + + self::assertEquals('2018-05-23 21:47:55', $timesheet->getBegin()->format('Y-m-d H:i:s')); + self::assertEquals('2018-05-24 01:11:11', $timesheet->getEnd()->format('Y-m-d H:i:s')); + self::assertEquals(12196, $timesheet->getDuration()); + } + + public function testCreateUseFromToDatetimeOverwritesBeginEndTatesFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'begin' => '2017-07-23', + 'end' => '2017-07-23', + 'from' => '2018-05-23 21:47:55', + 'to' => '2018-05-24 01:11:11', + ]); + + $sut->create($timesheet, $request); + + self::assertNotNull($timesheet->getBegin()); + self::assertNotNull($timesheet->getEnd()); + + self::assertEquals('2018-05-23 21:47:55', $timesheet->getBegin()->format('Y-m-d H:i:s')); + self::assertEquals('2018-05-24 01:11:11', $timesheet->getEnd()->format('Y-m-d H:i:s')); + self::assertEquals(12196, $timesheet->getDuration()); + } + + public function testCreateIgnoresValidToOnInvalidFromDatetimeFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'from' => '2018-xx-23 21:47:55', + 'to' => '2018-05-24 01:11:11', + ]); + + $sut->create($timesheet, $request); + + self::assertNull($timesheet->getBegin()); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } + + public function testCreateUsesFromAndIgnoresInvalidToDatetimeFromRequest() + { + $sut = $this->createSut(); + + $timesheet = new Timesheet(); + $request = new Request([ + 'from' => '2018-05-23 21:47:55', + 'to' => '2018-xx-24 01:11:11', + ]); + + $sut->create($timesheet, $request); + + self::assertEquals('2018-05-23 21:47:55', $timesheet->getBegin()->format('Y-m-d H:i:s')); + self::assertNull($timesheet->getEnd()); + self::assertEquals(0, $timesheet->getDuration()); + } +} diff --git a/tests/Timesheet/TrackingMode/DefaultModeTest.php b/tests/Timesheet/TrackingMode/DefaultModeTest.php new file mode 100644 index 00000000..72ab853c --- /dev/null +++ b/tests/Timesheet/TrackingMode/DefaultModeTest.php @@ -0,0 +1,45 @@ +create(); + $configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']); + + return new DefaultMode($dateTime, $configuration); + } + + public function testDefaultValues() + { + $sut = $this->createSut(); + + self::assertTrue($sut->canEditBegin()); + self::assertTrue($sut->canEditEnd()); + self::assertFalse($sut->canEditDuration()); + self::assertTrue($sut->canUpdateTimesWithAPI()); + self::assertTrue($sut->canSeeBeginAndEndTimes()); + self::assertEquals('default', $sut->getId()); + } +} diff --git a/tests/Timesheet/TrackingMode/DurationFixedStartModeTest.php b/tests/Timesheet/TrackingMode/DurationFixedStartModeTest.php new file mode 100644 index 00000000..58f3b528 --- /dev/null +++ b/tests/Timesheet/TrackingMode/DurationFixedStartModeTest.php @@ -0,0 +1,57 @@ +create(); + $configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']); + + return new DurationFixedStartMode($dateTime, $configuration); + } + + public function testDefaultValues() + { + $sut = $this->createSut(); + + self::assertFalse($sut->canEditBegin()); + self::assertFalse($sut->canEditEnd()); + self::assertTrue($sut->canEditDuration()); + self::assertFalse($sut->canUpdateTimesWithAPI()); + self::assertFalse($sut->canSeeBeginAndEndTimes()); + self::assertEquals('duration_fixed_start', $sut->getId()); + } + + public function testCreate() + { + $timesheet = new Timesheet(); + $timesheet->setBegin(new \DateTime('22:54')); + $request = new Request(); + + $sut = $this->createSut(); + self::assertEquals('22:54', $timesheet->getBegin()->format('H:i')); + $sut->create($timesheet, $request); + self::assertEquals('13:47', $timesheet->getBegin()->format('H:i')); + } +} diff --git a/tests/Timesheet/TrackingMode/DurationOnlyModeTest.php b/tests/Timesheet/TrackingMode/DurationOnlyModeTest.php new file mode 100644 index 00000000..dddb443c --- /dev/null +++ b/tests/Timesheet/TrackingMode/DurationOnlyModeTest.php @@ -0,0 +1,42 @@ +create(); + $configuration = new TimesheetConfiguration($loader, []); + + return new DurationOnlyMode($dateTime, $configuration); + } + + public function testDefaultValues() + { + $sut = $this->createSut(); + + self::assertTrue($sut->canEditBegin()); + self::assertFalse($sut->canEditEnd()); + self::assertTrue($sut->canEditDuration()); + self::assertTrue($sut->canUpdateTimesWithAPI()); + self::assertFalse($sut->canSeeBeginAndEndTimes()); + self::assertEquals('duration_only', $sut->getId()); + } +} diff --git a/tests/Timesheet/TrackingMode/PunchInOutModeTest.php b/tests/Timesheet/TrackingMode/PunchInOutModeTest.php new file mode 100644 index 00000000..2f975e38 --- /dev/null +++ b/tests/Timesheet/TrackingMode/PunchInOutModeTest.php @@ -0,0 +1,45 @@ +canEditBegin()); + self::assertFalse($sut->canEditEnd()); + self::assertFalse($sut->canEditDuration()); + self::assertFalse($sut->canUpdateTimesWithAPI()); + self::assertTrue($sut->canSeeBeginAndEndTimes()); + self::assertEquals('punch', $sut->getId()); + } + + public function testCreate() + { + $timesheet = new Timesheet(); + $timesheet->setBegin(new \DateTime('22:54')); + $request = new Request(); + $timesheetNew = clone $timesheet; + + $sut = new PunchInOutMode(); + $sut->create($timesheet, $request); + self::assertEquals($timesheet, $timesheetNew); + } +} diff --git a/tests/Timesheet/TrackingModeServiceTest.php b/tests/Timesheet/TrackingModeServiceTest.php new file mode 100644 index 00000000..e39f8d4d --- /dev/null +++ b/tests/Timesheet/TrackingModeServiceTest.php @@ -0,0 +1,71 @@ +create(); + $configuration = new TimesheetConfiguration($loader, ['mode' => 'punch']); + + $sut = new TrackingModeService($dateTime, $configuration); + + $modes = $sut->getModes(); + self::assertGreaterThanOrEqual(4, $modes); + + $ids = []; + foreach ($modes as $mode) { + $ids[] = $mode->getId(); + } + + self::assertContains('default', $ids); + self::assertContains('punch', $ids); + self::assertContains('duration_only', $ids); + self::assertContains('duration_fixed_start', $ids); + } + + public function testGetActiveMode() + { + $loader = new TestConfigLoader([]); + $dateTime = (new UserDateTimeFactoryFactory($this))->create(); + $configuration = new TimesheetConfiguration($loader, ['mode' => 'punch']); + + $sut = new TrackingModeService($dateTime, $configuration); + + self::assertInstanceOf(PunchInOutMode::class, $sut->getActiveMode()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @expectedExceptionMessage You have requested a non-existent service "xxxxxx" + */ + public function testGetActiveModeThrowsExceptionOnlyInvalidMode() + { + $loader = new TestConfigLoader([]); + $dateTime = (new UserDateTimeFactoryFactory($this))->create(); + $configuration = new TimesheetConfiguration($loader, ['mode' => 'xxxxxx']); + + $sut = new TrackingModeService($dateTime, $configuration); + + $sut->getActiveMode(); + } +} diff --git a/tests/Twig/TimesheetConfigExtensionTest.php b/tests/Twig/TimesheetConfigExtensionTest.php deleted file mode 100644 index 86b2ff16..00000000 --- a/tests/Twig/TimesheetConfigExtensionTest.php +++ /dev/null @@ -1,59 +0,0 @@ -getMockBuilder(ConfigLoaderInterface::class)->getMock(); - $config = new TimesheetConfiguration($loader, ['mode' => 'duration_only']); - $sut = new TimesheetConfigExtension($config); - $filters = $sut->getFunctions(); - $this->assertCount(2, $filters); - $this->assertEquals('is_duration_only', $filters[0]->getName()); - $this->assertEquals('is_punch_mode', $filters[1]->getName()); - } - - public function testIsDurationOnly() - { - $loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock(); - $config = new TimesheetConfiguration($loader, ['mode' => 'duration_only']); - $sut = new TimesheetConfigExtension($config); - $this->assertTrue($sut->isDurationOnly()); - $this->assertFalse($sut->isPunchInOut()); - } - - public function testIsNotDurationOnly() - { - $loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock(); - $config = new TimesheetConfiguration($loader, ['mode' => 'default']); - $sut = new TimesheetConfigExtension($config); - $this->assertFalse($sut->isDurationOnly()); - $this->assertFalse($sut->isPunchInOut()); - } - - public function testIsPunchInOut() - { - $loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock(); - $config = new TimesheetConfiguration($loader, ['mode' => 'punch']); - $sut = new TimesheetConfigExtension($config); - $this->assertFalse($sut->isDurationOnly()); - $this->assertTrue($sut->isPunchInOut()); - } -} diff --git a/tests/Validator/Constraints/TimesheetValidatorTest.php b/tests/Validator/Constraints/TimesheetValidatorTest.php index c51a6fff..6a9f3191 100644 --- a/tests/Validator/Constraints/TimesheetValidatorTest.php +++ b/tests/Validator/Constraints/TimesheetValidatorTest.php @@ -15,6 +15,8 @@ use App\Entity\Activity; use App\Entity\Customer; use App\Entity\Project; use App\Entity\Timesheet; +use App\Tests\Mocks\Security\UserDateTimeFactoryFactory; +use App\Timesheet\TrackingModeService; use App\Validator\Constraints\Timesheet as TimesheetConstraint; use App\Validator\Constraints\TimesheetValidator; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; @@ -38,8 +40,10 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase ], 'mode' => 'default', ]); + $dateTime = (new UserDateTimeFactoryFactory($this))->create(); + $service = new TrackingModeService($dateTime, $config); - return new TimesheetValidator($authMock, $config); + return new TimesheetValidator($authMock, $config, $service); } /** diff --git a/translations/system-configuration.de.xliff b/translations/system-configuration.de.xliff index 874da9db..6e9c0918 100644 --- a/translations/system-configuration.de.xliff +++ b/translations/system-configuration.de.xliff @@ -40,7 +40,11 @@ label.timesheet.mode_duration_only - [Dauer] ersetzt die Endzeit durch ein Eingabefeld für Dauer + [Dauer] ersetzt den Endzeitpunkt durch ein Eingabefeld für Dauer + + + label.timesheet.mode_duration_fixed_start + [Dauer] Konfigurierbare feste Startzeit, nur Dauer kann geändert werden label.timesheet.mode_punch diff --git a/translations/system-configuration.en.xliff b/translations/system-configuration.en.xliff index 0d5c62c3..4c69e744 100644 --- a/translations/system-configuration.en.xliff +++ b/translations/system-configuration.en.xliff @@ -42,6 +42,10 @@ label.timesheet.mode_duration_only [Duration] replaces the end time with a duration input-field + + label.timesheet.mode_duration_fixed_start + [Duration] Configurable fixed start-time, only duration can be changed + label.timesheet.mode_punch [Time-clock] user can start and stop records, but not edit the times or duration