diff --git a/src/Controller/Administrative/Availability/IndexController.php b/src/Controller/Administrative/Availability/IndexController.php index a710446..7c1053c 100644 --- a/src/Controller/Administrative/Availability/IndexController.php +++ b/src/Controller/Administrative/Availability/IndexController.php @@ -3,6 +3,7 @@ namespace App\Controller\Administrative\Availability; use App\Repository\AvailabilityRepository; +use App\Service\Common\AvailabilityFilterHandler; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -15,6 +16,7 @@ class IndexController extends AbstractController public function __construct( private readonly AvailabilityRepository $availabilityRepository, private readonly PaginatorInterface $paginator, + private readonly AvailabilityFilterHandler $filterHandler, ) { } @@ -22,9 +24,11 @@ class IndexController extends AbstractController #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(Request $request): Response { + $filterDto = $this->filterHandler->getFilterSettings(); + $query = $this ->availabilityRepository - ->getListQuery(true) + ->getListQuery(true, $filterDto) ; $pagination = $this->paginator->paginate( @@ -39,6 +43,7 @@ class IndexController extends AbstractController return $this->render('administrative/availability/index.html.twig', [ 'pagination' => $pagination, + 'filterDto' => $filterDto, ]); } } diff --git a/src/Controller/Common/AvailabilityFilterController.php b/src/Controller/Common/AvailabilityFilterController.php new file mode 100644 index 0000000..087b4e4 --- /dev/null +++ b/src/Controller/Common/AvailabilityFilterController.php @@ -0,0 +1,61 @@ +filterHandler->getFilterSettings(); + $formOptions = [ + 'job_profiles' => $this->jobProfileRepository->getList(), + ]; + + $form = $this->createForm(AvailabilityFilterType::class, $formData, $formOptions); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->filterHandler->handleRequest($form); + $returnUrl = $this->getReturnUrl($request, 'app_administrative_availability_index'); + $returnUrl = $this->removeQueryParameters($returnUrl, ['page']); + + return new HxRedirectResponse($returnUrl); + } + + return $this->render('common/modal_availability_filter.html.twig', [ + 'filterForm' => $form->createView(), + 'filterDto' => $form->getData(), + ]); + } + + #[Route('/common/availability/filter/reset', name: 'app_common_availability_filter_reset')] + #[IsGranted('ROLE_ADMINISTRATIVE')] + public function reset(Request $request): Response + { + $this->filterHandler->resetFilterSettings(); + $returnUrl = $this->getReturnUrl($request, 'app_administrative_availability_index'); + + return $this->redirect($returnUrl); + } +} diff --git a/src/Form/AvailabilityFilterType.php b/src/Form/AvailabilityFilterType.php new file mode 100644 index 0000000..7f79eb6 --- /dev/null +++ b/src/Form/AvailabilityFilterType.php @@ -0,0 +1,63 @@ +add('teamer', AutocompleteEntityType::class, [ + 'label' => 'Teamer', + 'class' => Teamer::class, + 'label_property' => 'name', + 'label_function' => fn (Teamer $teamer) => (string) $teamer, + 'endpoint_route' => 'app_admin_autocomplete_teamer', + ]) + ->add('jobProfiles', MultiselectEntityType::class, [ + 'label' => 'Job-Profil', + 'class' => JobProfile::class, + 'required' => false, + 'empty_label' => 'nicht filtern', + 'choice_label' => 'name', + 'choices' => $options['job_profiles'], + ]) + ->add('dateFrom', DatepickerType::class, [ + 'label' => 'Zeitraum von', + 'attr' => [ + 'placeholder' => 'nicht filtern', + ], + ]) + ->add('dateTo', DatepickerType::class, [ + 'label' => 'Zeitraum bis', + 'attr' => [ + 'placeholder' => 'nicht filtern', + ], + ]) + ->add('apply', SubmitType::class, [ + 'label' => 'filtern', + ]) + ->add('reset', SubmitType::class, [ + 'label' => 'reset', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => AvailabilityFilterDto::class, + 'job_profiles' => [], + ]) + ; + } +} diff --git a/src/Model/AvailabilityFilterDto.php b/src/Model/AvailabilityFilterDto.php new file mode 100644 index 0000000..0615662 --- /dev/null +++ b/src/Model/AvailabilityFilterDto.php @@ -0,0 +1,61 @@ +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 getTeamer(): ?Teamer + { + return $this->teamer; + } + + public function setTeamer(?Teamer $teamer): static + { + $this->teamer = $teamer; + + return $this; + } + + public function getJobProfiles(): array + { + return $this->jobProfiles; + } + + public function setJobProfiles(array $jobProfiles): static + { + $this->jobProfiles = $jobProfiles; + + return $this; + } +} diff --git a/src/Repository/AvailabilityRepository.php b/src/Repository/AvailabilityRepository.php index 643ab60..31f3641 100644 --- a/src/Repository/AvailabilityRepository.php +++ b/src/Repository/AvailabilityRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\Availability; use App\Entity\Teamer; +use App\Model\AvailabilityFilterDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; @@ -23,7 +24,7 @@ class AvailabilityRepository extends ServiceEntityRepository parent::__construct($registry, Availability::class); } - public function getListQuery(bool $custom = false): Query + public function getListQuery(bool $custom = false, ?AvailabilityFilterDto $filterDto = null): Query { $qb = $this->createQueryBuilder('availability'); @@ -49,6 +50,50 @@ class AvailabilityRepository extends ServiceEntityRepository ; } + if (null === $filterDto) { + return $qb->getQuery(); + } + + if (null !== $dateFrom = $filterDto->getDateFrom()) { + $qb + ->andWhere($qb->expr()->gte('availability.dateFrom', ':dateFrom')) + ->setParameter('dateFrom', $dateFrom) + ; + } + + if (null !== $dateTo = $filterDto->getDateTo()) { + $qb + ->andWhere($qb->expr()->lte('availability.dateTo', ':dateTo')) + ->setParameter('dateTo', $dateTo) + ; + } + + if (null !== $teamer = $filterDto->getTeamer()) { + $qb + ->andWhere($qb->expr()->eq('availability.owner', ':teamer')) + ->setParameter('teamer', $teamer) + ; + } + + if (0 < count($jobProfiles = $filterDto->getJobProfiles())) { + // a join on the job-profiles would multiply the availability whenever its + // owner holds more than one of the selected profiles, which would break + // the pagination count, so the owner is matched against a subquery instead + $subQuery = $this + ->getEntityManager() + ->createQueryBuilder() + ->select('profileTeamer.id') + ->from(Teamer::class, 'profileTeamer') + ->innerJoin('profileTeamer.jobProfiles', 'filterJobProfile') + ->where('filterJobProfile IN (:jobProfiles)') + ; + + $qb + ->andWhere($qb->expr()->in('availability.owner', $subQuery->getDQL())) + ->setParameter('jobProfiles', $jobProfiles) + ; + } + return $qb->getQuery(); } diff --git a/src/Service/Common/AvailabilityFilterHandler.php b/src/Service/Common/AvailabilityFilterHandler.php new file mode 100644 index 0000000..da81867 --- /dev/null +++ b/src/Service/Common/AvailabilityFilterHandler.php @@ -0,0 +1,61 @@ +modelClass(); + + if (null === $data = $this->getSession()->get($this->namespace)) { + return $filterDto; + } + + $filterDto + ->setDateFrom($data['date_from']) + ->setDateTo($data['date_to']) + ; + + if (isset($data['teamer_id'])) { + $teamer = $this + ->entityManager + ->getRepository(Teamer::class) + ->find($data['teamer_id']) + ; + $filterDto->setTeamer($teamer); + } + + if (isset($data['job_profiles']) && 0 < count($data['job_profiles'])) { + $jobProfiles = $this + ->entityManager + ->getRepository(JobProfile::class) + ->findBy(['id' => $data['job_profiles']]) + ; + $filterDto->setJobProfiles($jobProfiles); + } + + return $filterDto; + } + + protected function saveFilterSettings(AbstractFilterDto $filterDto): void + { + /* @var AvailabilityFilterDto $filterDto */ + $this->getSession()->set($this->namespace, [ + 'date_from' => $filterDto->getDateFrom(), + 'date_to' => $filterDto->getDateTo(), + 'teamer_id' => $filterDto->getTeamer()?->getId(), + 'job_profiles' => array_map(function (JobProfile $jobProfile) { + return $jobProfile->getId(); + }, $filterDto->getJobProfiles()), + ]); + } +} diff --git a/templates/administrative/availability/index.html.twig b/templates/administrative/availability/index.html.twig index d4cbe6b..481a53a 100644 --- a/templates/administrative/availability/index.html.twig +++ b/templates/administrative/availability/index.html.twig @@ -3,9 +3,32 @@ {% block title %}Eigene Verfügbarkeiten der Teamer{% endblock %} {% block content %} -
| + | Keine Daten... | |||||