From e61ffe4db46fe09c9838e8093775df8b023bb7a2 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 18 May 2020 23:35:43 +0200 Subject: [PATCH] create timesheet for multiple users (#1716) --- UPGRADING.md | 1 + .../TimesheetAbstractController.php | 2 +- src/Controller/TimesheetTeamController.php | 88 ++++++++++++++++++- src/Entity/Timesheet.php | 21 +++++ src/Form/Model/MultiUserTimesheet.php | 86 ++++++++++++++++++ src/Form/TimesheetMultiUserEditForm.php | 43 +++++++++ src/Form/Type/PageSizeType.php | 1 + src/Form/Type/TeamMemberType.php | 68 ++++++++++++++ src/Form/Type/UserType.php | 1 + .../Constraints/TimesheetMultiUser.php | 33 +++++++ .../TimesheetMultiUserValidator.php | 47 ++++++++++ templates/macros/widgets.html.twig | 22 ++--- templates/timesheet-team/actions.html.twig | 2 +- templates/timesheet/layout-edit.html.twig | 21 +++++ .../TimesheetTeamControllerTest.php | 62 ++++++++++++- .../TimesheetMultiUserValidatorTest.php | 50 +++++++++++ translations/actions.de.xlf | 4 + translations/actions.en.xlf | 4 + translations/validators.de.xlf | 4 + translations/validators.en.xlf | 4 + 20 files changed, 548 insertions(+), 16 deletions(-) create mode 100644 src/Form/Model/MultiUserTimesheet.php create mode 100644 src/Form/TimesheetMultiUserEditForm.php create mode 100644 src/Form/Type/TeamMemberType.php create mode 100644 src/Validator/Constraints/TimesheetMultiUser.php create mode 100644 src/Validator/Constraints/TimesheetMultiUserValidator.php create mode 100644 tests/Validator/Constraints/TimesheetMultiUserValidatorTest.php diff --git a/UPGRADING.md b/UPGRADING.md index b7363d41..3aed41c4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -15,6 +15,7 @@ Perform EACH version specific task between your version and the new one, otherwi ### Developer - **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed +- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more ## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9) diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index ec66d1f1..8fc12e0c 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -61,7 +61,7 @@ abstract class TimesheetAbstractController extends AbstractController /** * @var TimesheetService */ - private $service; + protected $service; public function __construct( UserDateTimeFactory $dateTime, diff --git a/src/Controller/TimesheetTeamController.php b/src/Controller/TimesheetTeamController.php index 9cc5394f..d36e03dc 100644 --- a/src/Controller/TimesheetTeamController.php +++ b/src/Controller/TimesheetTeamController.php @@ -9,14 +9,22 @@ namespace App\Controller; +use App\Entity\Tag; +use App\Entity\Team; use App\Entity\Timesheet; use App\Event\TimesheetMetaDisplayEvent; +use App\Form\Model\MultiUserTimesheet; use App\Form\TimesheetAdminEditForm; +use App\Form\TimesheetMultiUserEditForm; use App\Repository\ActivityRepository; use App\Repository\ProjectRepository; use App\Repository\Query\TimesheetQuery; use App\Repository\TagRepository; +use App\Timesheet\TrackingMode\TrackingModeInterface; +use Doctrine\Common\Collections\ArrayCollection; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -59,7 +67,7 @@ class TimesheetTeamController extends TimesheetAbstractController * * @param Timesheet $entry * @param Request $request - * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response + * @return RedirectResponse|Response */ public function editAction(Timesheet $entry, Request $request) { @@ -73,13 +81,89 @@ class TimesheetTeamController extends TimesheetAbstractController * @param Request $request * @param ProjectRepository $projectRepository * @param ActivityRepository $activityRepository - * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response + * @return RedirectResponse|Response */ public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository) { return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository); } + /** + * @Route(path="/create_mu", name="admin_timesheet_create_multiuser", methods={"GET", "POST"}) + * @Security("is_granted('create_other_timesheet')") + * + * @param Request $request + * @return RedirectResponse|Response + */ + public function createForMultiUserAction(Request $request) + { + $entry = new MultiUserTimesheet(); + $entry->setUser($this->getUser()); + $this->service->prepareNewTimesheet($entry, $request); + + $mode = $this->getTrackingMode(); + $createForm = $this->getMultiUserCreateForm($entry, $mode); + $createForm->handleRequest($request); + + if ($createForm->isSubmitted() && $createForm->isValid()) { + try { + /** @var ArrayCollection $users */ + $users = $createForm->get('users')->getData(); + /** @var ArrayCollection $teams */ + $teams = $createForm->get('teams')->getData(); + + $allUsers = $users->toArray(); + foreach ($teams as $team) { + $allUsers = array_merge($allUsers, $team->getUsers()->toArray()); + } + $allUsers = array_unique($allUsers); + + /** @var Tag[] $tags */ + $tags = []; + /** @var Tag $tag */ + foreach ($entry->getTags() as $tag) { + $tag->removeTimesheet($entry); + $tags[] = $tag; + } + + foreach ($allUsers as $user) { + $newTimesheet = $entry->createCopy(); + $newTimesheet->setUser($user); + foreach ($tags as $tag) { + $newTimesheet->addTag($tag); + } + $this->service->prepareNewTimesheet($newTimesheet, $request); + $this->service->saveNewTimesheet($newTimesheet); + } + + $this->flashSuccess('action.update.success'); + + return $this->redirectToRoute($this->getTimesheetRoute()); + } catch (\Exception $ex) { + $this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]); + } + } + + return $this->render('timesheet-team/edit.html.twig', [ + 'timesheet' => $entry, + 'form' => $createForm->createView(), + ]); + } + + protected function getMultiUserCreateForm(MultiUserTimesheet $entry, TrackingModeInterface $mode): FormInterface + { + return $this->createForm(TimesheetMultiUserEditForm::class, $entry, [ + 'action' => $this->generateUrl('admin_timesheet_create_multiuser'), + 'include_rate' => $this->isGranted('edit_rate', $entry), + 'include_exported' => $this->isGranted('edit_export', $entry), + 'include_user' => $this->includeUserInForms('create'), + 'allow_begin_datetime' => $mode->canEditBegin(), + 'allow_end_datetime' => $mode->canEditEnd(), + 'allow_duration' => $mode->canEditDuration(), + 'customer' => true, + ]); + } + /** * @Route(path="/multi-update", name="admin_timesheet_multi_update", methods={"POST"}) * @Security("is_granted('edit_other_timesheet')") diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 8f49d53b..a522fe2c 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -557,6 +557,27 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface return $this; } + public function createCopy(?Timesheet $timesheet = null): Timesheet + { + if (null === $timesheet) { + $timesheet = new Timesheet(); + } + + $values = get_object_vars($this); + foreach ($values as $k => $v) { + $timesheet->$k = $v; + } + + $timesheet->meta = new ArrayCollection(); + + /** @var TimesheetMeta $meta */ + foreach ($this->meta as $meta) { + $timesheet->setMetaField(clone $meta); + } + + return $timesheet; + } + public function __clone() { if ($this->id) { diff --git a/src/Form/Model/MultiUserTimesheet.php b/src/Form/Model/MultiUserTimesheet.php new file mode 100644 index 00000000..8c1bb2f9 --- /dev/null +++ b/src/Form/Model/MultiUserTimesheet.php @@ -0,0 +1,86 @@ + + */ + private $users; + /** + * @var Collection + */ + private $teams; + + public function __construct() + { + parent::__construct(); + $this->users = new ArrayCollection(); + $this->teams = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getUsers(): Collection + { + return $this->users; + } + + public function addUser(User $user) + { + $this->users->add($user); + + return $this; + } + + public function removeUser(User $user) + { + if ($this->users->contains($user)) { + $this->users->remove($user); + } + + return $this; + } + + /** + * @return Collection + */ + public function getTeams(): Collection + { + return $this->teams; + } + + public function addTeam(Team $team) + { + $this->teams->add($team); + + return $this; + } + + public function removeTeam(Team $team) + { + if ($this->teams->contains($team)) { + $this->teams->remove($team); + } + + return $this; + } +} diff --git a/src/Form/TimesheetMultiUserEditForm.php b/src/Form/TimesheetMultiUserEditForm.php new file mode 100644 index 00000000..6869ebb3 --- /dev/null +++ b/src/Form/TimesheetMultiUserEditForm.php @@ -0,0 +1,43 @@ +add('users', TeamMemberType::class, [ + 'multiple' => true, + 'required' => false, + ]); + + $builder->add('teams', TeamType::class, [ + 'multiple' => true, + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + } +} diff --git a/src/Form/Type/PageSizeType.php b/src/Form/Type/PageSizeType.php index 3145e061..83a19f9b 100644 --- a/src/Form/Type/PageSizeType.php +++ b/src/Form/Type/PageSizeType.php @@ -37,6 +37,7 @@ class PageSizeType extends AbstractType 500 => 500 ], 'placeholder' => null, + 'choice_translation_domain' => false, ]); } diff --git a/src/Form/Type/TeamMemberType.php b/src/Form/Type/TeamMemberType.php new file mode 100644 index 00000000..e20f5848 --- /dev/null +++ b/src/Form/Type/TeamMemberType.php @@ -0,0 +1,68 @@ +setDefaults([ + 'class' => User::class, + 'label' => 'label.user', + 'choice_label' => function (User $user) { + return $user->getDisplayName(); + }, + ]); + + $resolver->setDefault('query_builder', function (Options $options) { + return function (UserRepository $repo) use ($options) { + $qb = $repo->createQueryBuilder('u'); + $qb + ->andWhere($qb->expr()->eq('u.enabled', ':enabled')) + ->setParameter('enabled', true, \PDO::PARAM_BOOL) + ->orderBy('u.username', 'ASC'); + + /** @var User $user */ + $user = $options['user']; + + if (null !== $user && !$user->getTeams()->isEmpty() && !$user->isSuperAdmin() && !$user->isAdmin()) { + $qb + ->leftJoin('u.teams', 'teams') + ->leftJoin('teams.users', 'users') + ->andWhere($qb->expr()->isMemberOf(':teams', 'u.teams')) + ->setParameter('teams', $user->getTeams()); + } + + return $qb; + }; + }); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return EntityType::class; + } +} diff --git a/src/Form/Type/UserType.php b/src/Form/Type/UserType.php index e3cffb4b..0d3e1f6e 100644 --- a/src/Form/Type/UserType.php +++ b/src/Form/Type/UserType.php @@ -33,6 +33,7 @@ class UserType extends AbstractType 'choice_label' => function (User $user) { return $user->getDisplayName(); }, + 'choice_translation_domain' => false, ]); $resolver->setDefault('query_builder', function (Options $options) { diff --git a/src/Validator/Constraints/TimesheetMultiUser.php b/src/Validator/Constraints/TimesheetMultiUser.php new file mode 100644 index 00000000..7253e6cc --- /dev/null +++ b/src/Validator/Constraints/TimesheetMultiUser.php @@ -0,0 +1,33 @@ + 'You must select at least one user or team.', + ]; + + public $message = 'This form has invalid settings.'; + + public function getTargets() + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Validator/Constraints/TimesheetMultiUserValidator.php b/src/Validator/Constraints/TimesheetMultiUserValidator.php new file mode 100644 index 00000000..6856aec3 --- /dev/null +++ b/src/Validator/Constraints/TimesheetMultiUserValidator.php @@ -0,0 +1,47 @@ +getTeams()->isEmpty() && $value->getUsers()->isEmpty()) { + $this->context->buildViolation('You must select at least one user or team.') + ->atPath('users') + ->setTranslationDomain('validators') + ->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM) + ->addViolation(); + + $this->context->buildViolation('You must select at least one user or team.') + ->atPath('teams') + ->setTranslationDomain('validators') + ->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM) + ->addViolation(); + } + } +} diff --git a/templates/macros/widgets.html.twig b/templates/macros/widgets.html.twig index a9e233b9..3d066477 100644 --- a/templates/macros/widgets.html.twig +++ b/templates/macros/widgets.html.twig @@ -37,9 +37,9 @@ {% macro label_boolean(visible) %} {% import _self as macro %} {% if visible %} - {{ macro.label('yes', 'success') }} + {{ macro.label('yes'|trans, 'success') }} {% else %} - {{ macro.label('no', 'default') }} + {{ macro.label('no'|trans, 'default') }} {% endif %} {% endmacro %} @@ -50,17 +50,17 @@ {% macro label_role(role) %} {% import _self as macro %} + {% set color = 'primary' %} {% if role == 'ROLE_SUPER_ADMIN' %} - {{ macro.label(role, 'danger') }} + {% set color = 'danger' %} {% elseif role == 'ROLE_ADMIN' %} - {{ macro.label(role, 'warning') }} + {% set color = 'warning' %} {% elseif role == 'ROLE_TEAMLEAD' %} - {{ macro.label(role, 'success') }} + {% set color = 'success' %} {% elseif role == 'ROLE_USER' %} - {{ macro.label(role, 'gray') }} - {% else %} - {{ macro.label(role, 'primary') }} + {% set color = 'gray' %} {% endif %} + {{ macro.label(role|trans, color) }} {% endmacro %} {% macro username(user) %} @@ -155,11 +155,11 @@ {% macro label(title, type, tooltip) %} {# success, warning, danger, primary #} - {{ title|trans }} + {{ title }} {% endmacro %} {% macro badge(title, color) %} - {{ title|trans }} + {{ title }} {% endmacro %} {% macro alert(type, description, title, icon) %} @@ -345,7 +345,7 @@ {{ 'label.toggle_dropdown'|trans }} -