From 6f7db829b5a8024801e99ca154c9effc651f9717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 23 Oct 2023 15:03:17 +0200 Subject: [PATCH] Feat: Add assignment filter for admins --- config/packages/doctrine.yaml | 2 + .../Admin/Assignment/IndexController.php | 13 +- src/Controller/Teamer/IndexController.php | 2 +- src/Form/AssignmentFilterType.php | 94 ++++++++++++++ src/Model/AssignmentFilterDto.php | 93 ++++++++++++++ src/Repository/AssignmentRepository.php | 110 +++++++++++++---- .../Common/AssignmentFilterHandler.php | 116 ++++++++++++++++++ templates/admin/assignment/index.html.twig | 24 +++- templates/forms.html.twig | 2 +- 9 files changed, 424 insertions(+), 32 deletions(-) create mode 100644 src/Form/AssignmentFilterType.php create mode 100644 src/Model/AssignmentFilterDto.php create mode 100644 src/Service/Common/AssignmentFilterHandler.php diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index cdec02d..2a3a002 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -22,6 +22,8 @@ doctrine: dql: string_functions: DATE_FORMAT: DoctrineExtensions\Query\Mysql\DateFormat + datetime_functions: + DATEDIFF: DoctrineExtensions\Query\Mysql\DateDiff when@test: doctrine: dbal: diff --git a/src/Controller/Admin/Assignment/IndexController.php b/src/Controller/Admin/Assignment/IndexController.php index b670519..8cae274 100644 --- a/src/Controller/Admin/Assignment/IndexController.php +++ b/src/Controller/Admin/Assignment/IndexController.php @@ -3,6 +3,7 @@ namespace App\Controller\Admin\Assignment; use App\Repository\AssignmentRepository; +use App\Service\Common\AssignmentFilterHandler; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -14,7 +15,8 @@ class IndexController extends AbstractController { public function __construct( private readonly AssignmentRepository $assignmentRepository, - private readonly PaginatorInterface $paginator + private readonly PaginatorInterface $paginator, + private readonly AssignmentFilterHandler $filterHandler ) { } @@ -22,9 +24,15 @@ class IndexController extends AbstractController #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(Request $request): Response { + $filterForm = $this->filterHandler->getForm(); + $filterDto = $this->filterHandler->handleRequest($filterForm, $request); + if (true === $filterDto->isReset()) { + return $this->redirectToRoute('app_admin_assignment_index'); + } + $query = $this ->assignmentRepository - ->getListQuery() + ->getListQuery($filterDto) ; $pagination = $this->paginator->paginate( @@ -39,6 +47,7 @@ class IndexController extends AbstractController return $this->render('admin/assignment/index.html.twig', [ 'pagination' => $pagination, + 'filterForm' => $filterForm, ]); } } \ No newline at end of file diff --git a/src/Controller/Teamer/IndexController.php b/src/Controller/Teamer/IndexController.php index a25057d..10072aa 100644 --- a/src/Controller/Teamer/IndexController.php +++ b/src/Controller/Teamer/IndexController.php @@ -34,7 +34,7 @@ class IndexController extends AbstractController $query = $this ->assignmentRepository - ->getListQuery($teamer) + ->getListQueryForTeamer($teamer) ; $pagination = $this->paginator->paginate( diff --git a/src/Form/AssignmentFilterType.php b/src/Form/AssignmentFilterType.php new file mode 100644 index 0000000..65c954d --- /dev/null +++ b/src/Form/AssignmentFilterType.php @@ -0,0 +1,94 @@ +add('dateFrom', DatepickerType::class, [ + 'label' => false, + 'min_date' => $options['min_date'], + 'max_date' => $options['max_date'], + 'attr' => [ + 'placeholder' => 'von', + ], + ]) + ->add('dateTo', DatepickerType::class, [ + 'label' => false, + 'min_date' => $options['min_date'], + 'max_date' => $options['max_date'], + 'attr' => [ + 'placeholder' => 'bis', + ], + ]) + ->add('duration', ChoiceType::class, [ + 'label' => false, + 'placeholder' => 'Einsatzdauer', + 'choices' => [ + 'Wochenende (2-4 Tage)' => AssignmentFilterDto::DURATION_WEEKEND, + 'Midweek (5-6 Tage)' => AssignmentFilterDto::DURATION_MID_WEEK, + 'Ganze Woche (7 Tage)' => AssignmentFilterDto::DURATION_FULL_WEEK, + 'Mehr als einen Woche' => AssignmentFilterDto::DURATION_MORE, + ], + ]) + ->add('apply', SubmitType::class, [ + 'label' => 'anwenden', + ]) + ->add('reset', SubmitType::class, [ + 'label' => 'reset', + ]) + ; + + if (0 < count($options['job_profiles'])) { + $builder + ->add('jobProfile', EntityType::class, [ + 'label' => false, + 'placeholder' => 'Job-Profil', + 'class' => JobProfile::class, + 'choice_label' => 'name', + 'choices' => $options['job_profiles'], + ]) + ; + } + if (0 < count($options['destinations'])) { + $builder + ->add('destination', EntityType::class, [ + 'label' => false, + 'placeholder' => 'Destination', + 'class' => Destination::class, + 'choice_label' => function (Destination $destination) { + return $destination->getProduct(); + }, + 'choices' => $options['destinations'], + ]) + ; + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => AssignmentFilterDto::class, + 'min_date' => null, + 'max_date' => null, + 'job_profiles' => [], + 'destinations' => [], + ]) + ->setAllowedTypes('min_date', [\DateTimeImmutable::class, 'null']) + ->setAllowedTypes('max_date', [\DateTimeImmutable::class, 'null']) + ; + } +} \ No newline at end of file diff --git a/src/Model/AssignmentFilterDto.php b/src/Model/AssignmentFilterDto.php new file mode 100644 index 0000000..9636135 --- /dev/null +++ b/src/Model/AssignmentFilterDto.php @@ -0,0 +1,93 @@ +dateFrom; + } + + public function setDateFrom(?\DateTimeImmutable $dateFrom): static + { + $this->dateFrom = $dateFrom; + + return $this; + } + + public function getDateTo(): ?\DateTimeImmutable + { + return $this->dateTo; + } + + public function setDateTo(?\DateTimeImmutable $dateTo): static + { + $this->dateTo = $dateTo; + + return $this; + } + + public function getJobProfile(): ?JobProfile + { + return $this->jobProfile; + } + + public function setJobProfile(?JobProfile $jobProfile): static + { + $this->jobProfile = $jobProfile; + + return $this; + } + + public function getDestination(): ?Destination + { + return $this->destination; + } + + public function setDestination(?Destination $destination): static + { + $this->destination = $destination; + + return $this; + } + + public function getDuration(): ?int + { + return $this->duration; + } + + public function setDuration(?int $duration): static + { + $this->duration = $duration; + + return $this; + } + + public function isReset(): bool + { + return $this->reset; + } + + public function setReset(bool $reset): static + { + $this->reset = $reset; + + return $this; + } +} \ No newline at end of file diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 2c7ef33..a1f5a30 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -4,7 +4,9 @@ namespace App\Repository; use App\Entity\Application; use App\Entity\Assignment; +use App\Entity\Destination; use App\Entity\Teamer; +use App\Model\AssignmentFilterDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\ORM\Query\Expr\Join; @@ -25,7 +27,7 @@ class AssignmentRepository extends ServiceEntityRepository parent::__construct($registry, Assignment::class); } - public function getListQuery(Teamer $teamer = null): Query + public function getListQuery(AssignmentFilterDto $filterDto): Query { $qb = $this->createQueryBuilder('assignment'); @@ -34,25 +36,81 @@ class AssignmentRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->innerJoin('assignment.jobProfile', 'job_profile') ->where($qb->expr()->isNull('assignment.deletedAt')) - ; - - if (null !== $teamer) { - $qb - ->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer')) - ->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer')) - ->setParameter('teamer', $teamer) + ->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->neq('application.status', ':status')) + ->leftJoin('assignment.dispositions', 'disposition') + ->setParameter('status', Application::STATUS_REJECTED) ; - } else { + + if (null !== $dateFrom = $filterDto->getDateFrom()) { $qb - ->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->neq('application.status', ':status')) - ->leftJoin('assignment.dispositions', 'disposition') - ->setParameter('status', Application::STATUS_REJECTED) + ->andWhere($qb->expr()->gte('destination.dateFrom', ':dateFrom')) + ->setParameter('dateFrom', $dateFrom) ; } + if (null !== $dateTo = $filterDto->getDateTo()) { + $qb + ->andWhere($qb->expr()->lte('destination.dateTo', ':dateTo')) + ->setParameter('dateTo', $dateTo) + ; + } + + if (null !== $jobProfile = $filterDto->getJobProfile()) { + $qb + ->andWhere($qb->expr()->eq('assignment.jobProfile', ':jobProfile')) + ->setParameter('jobProfile', $jobProfile) + ; + } + + if (null !== $destination = $filterDto->getDestination()) { + $qb + ->andWhere($qb->expr()->eq('assignment.destination', ':destination')) + ->setParameter('destination', $destination) + ; + } + + if (null !== $duration = $filterDto->getDuration()) { + $days = match ($duration) { + AssignmentFilterDto::DURATION_WEEKEND => [2, 4], + AssignmentFilterDto::DURATION_MID_WEEK => [5, 6], + AssignmentFilterDto::DURATION_FULL_WEEK => [7, 7], + default => [8, 0], + }; + + [$minDays, $maxDays] = $days; + + $qb + ->andWhere($qb->expr()->gte('DATEDIFF(destination.dateTo, destination.dateFrom)', ':minDays')) + ->setParameter('minDays', $minDays) + ; + + if (0 < $maxDays) { + $qb + ->andWhere($qb->expr()->lte('DATEDIFF(destination.dateTo, destination.dateFrom)', ':maxDays')) + ->setParameter('maxDays', $maxDays) + ; + } + } + return $qb->getQuery(); } + public function getListQueryForTeamer(Teamer $teamer = null): Query + { + $qb = $this->createQueryBuilder('assignment'); + + return $qb + ->select('assignment', 'destination', 'job_profile', 'application', 'disposition') + ->innerJoin('assignment.destination', 'destination') + ->innerJoin('assignment.jobProfile', 'job_profile') + ->where($qb->expr()->isNull('assignment.deletedAt')) + ->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer')) + ->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer')) + ->setParameter('teamer', $teamer) + ->getQuery() + ; + } + public function getBookmarkQueryForTeamer(Teamer $teamer): Query { $qb = $this->createQueryBuilder('assignment'); @@ -85,39 +143,37 @@ class AssignmentRepository extends ServiceEntityRepository ->getSingleResult() ; - $options['minDate'] = $result['minDate']; - $options['maxDate'] = $result['maxDate']; + $options['minDate'] = new \DateTimeImmutable($result['minDate']); + $options['maxDate'] = new \DateTimeImmutable($result['maxDate']); // Job-profiles $qb = $this->createQueryBuilder('assignment'); $result = $qb - ->select('job_profile.id', 'job_profile.name') + ->select('assignment', 'job_profile') ->innerJoin('assignment.jobProfile', 'job_profile') ->orderBy('job_profile.name', 'ASC') ->where($qb->expr()->isNull('assignment.deletedAt')) ->getQuery() - ->getArrayResult() + ->getResult() ; + $options['jobProfiles'] = array_map(function (Assignment $assignment) { + return $assignment->getJobProfile(); + }, $result); - foreach ($result as $row) { - $options['jobProfiles'][$row['id']] = $row['name']; - } - - // Products + // Destinations $qb = $this->createQueryBuilder('assignment'); $result = $qb - ->select('destination.id id', 'destination.product product', 'destination.hotel hotel') + ->select('assignment', 'destination') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->isNull('assignment.deletedAt')) ->orderBy('destination.product', 'ASC') ->groupBy('destination.id') ->getQuery() - ->getArrayResult() + ->getResult() ; - - foreach ($result as $row) { - $options['products'][$row['id']] = $row['product']; - } + $options['destinations'] = array_map(function (Assignment $assignment) { + return $assignment->getDestination(); + }, $result); return $options; } diff --git a/src/Service/Common/AssignmentFilterHandler.php b/src/Service/Common/AssignmentFilterHandler.php new file mode 100644 index 0000000..d9270e0 --- /dev/null +++ b/src/Service/Common/AssignmentFilterHandler.php @@ -0,0 +1,116 @@ +getFilterSettings(); + + $assignmentRepository = $this->entityManager->getRepository(Assignment::class); + $filterOptions = $assignmentRepository->getFilterOptions(); + + return $this->formFactory->create(AssignmentFilterType::class, $filterDto, [ + 'min_date' => $filterOptions['minDate'], + 'max_date' => $filterOptions['maxDate'], + 'job_profiles' => $filterOptions['jobProfiles'], + 'destinations' => $filterOptions['destinations'], + ]); + } + + public function handleRequest(FormInterface $form, Request $request): AssignmentFilterDto + { + $form->handleRequest($request); + $filterDto = $form->getData(); + + if ($form->has('reset') && $form->get('reset')->isClicked()) { + $filterDto = new AssignmentFilterDto(); + $filterDto->setReset(true); + $this->getSession()->remove('assignment_filter'); + } elseif ($form->has('apply') && $form->get('apply')->isClicked()) { + $this->saveFilterSettings($filterDto); + } + + return $filterDto; + } + + public function getFilterSettings(): AssignmentFilterDto + { + if (null === $this->getSession()->get('assignment_filter')) { + return new AssignmentFilterDto(); + } + + return $this->loadFilterSettings(); + } + + public function loadFilterSettings(): AssignmentFilterDto + { + $data = $this->getSession()->get('assignment_filter'); + + $filterDto = new AssignmentFilterDto(); + + if (isset($data['date_from'])) { + $filterDto->setDateFrom(new \DateTimeImmutable($data['date_from'])); + } + if (isset($data['date_to'])) { + $filterDto->setDateTo(new \DateTimeImmutable($data['date_to'])); + } + if (isset($data['job_profile']) && 0 < (int) $data['job_profile']) { + $jobProfile = $this + ->entityManager + ->getRepository(JobProfile::class) + ->find($data['job_profile']) + ; + $filterDto->setJobProfile($jobProfile); + } + if (isset($data['destination']) && 0 < (int) $data['destination']) { + $destination = $this + ->entityManager + ->getRepository(Destination::class) + ->find($data['destination']) + ; + $filterDto->setDestination($destination); + } + if (isset($data['duration']) && 0 < (int) $data['duration']) { + $filterDto->setDuration($data['duration']); + } + + return $filterDto; + } + + public function saveFilterSettings(AssignmentFilterDto $filterDto): void + { + $this->getSession()->set('assignment_filter', [ + 'date_from' => $filterDto->getDateFrom()?->format('Y-m-d'), + 'date_to' => $filterDto->getDateTo()?->format('Y-m-d'), + 'job_profile' => $filterDto->getJobProfile()?->getId(), + 'destination' => $filterDto->getDestination()?->getId(), + 'duration' => $filterDto->getDuration(), + ]); + } + + private function getSession(): SessionInterface + { + return $this->requestStack->getSession(); + } +} \ No newline at end of file diff --git a/templates/admin/assignment/index.html.twig b/templates/admin/assignment/index.html.twig index 1274889..9779082 100644 --- a/templates/admin/assignment/index.html.twig +++ b/templates/admin/assignment/index.html.twig @@ -3,9 +3,25 @@ {% block title %}Einsatzübersicht{% endblock %} {% block content %} -

+

Einsatzübersicht

+ {{ form_start(filterForm) }} +
+
+ {{ form_widget(filterForm.dateFrom) }} + {{ form_widget(filterForm.dateTo) }} + {{ form_widget(filterForm.jobProfile) }} + {{ form_widget(filterForm.destination) }} + {{ form_widget(filterForm.duration) }} +
+
+ {{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn btn--small' } }) }} + {{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--small btn--secondary' } }) }} +
+
+ {{ form_rest(filterForm) }} + {{ form_end(filterForm) }}
@@ -88,6 +104,12 @@ + {% else %} + + + {% endfor %}
+ Keine Daten... +
diff --git a/templates/forms.html.twig b/templates/forms.html.twig index 70b0981..4314cdd 100644 --- a/templates/forms.html.twig +++ b/templates/forms.html.twig @@ -164,7 +164,7 @@ {%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%} {%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%} {%- if errors|length -%} - {%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%} + {%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' }) -%} {% else %} {%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%} {%- endif -%}