diff --git a/src/Controller/Admin/Application/IndexController.php b/src/Controller/Admin/Application/IndexController.php index 52ba603..ba84c1b 100644 --- a/src/Controller/Admin/Application/IndexController.php +++ b/src/Controller/Admin/Application/IndexController.php @@ -3,6 +3,7 @@ namespace App\Controller\Admin\Application; use App\Repository\ApplicationRepository; +use App\Service\Common\ApplicationFilterHandler; 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 ApplicationRepository $applicationRepository, - private readonly PaginatorInterface $paginator + private readonly PaginatorInterface $paginator, + private readonly ApplicationFilterHandler $filterHandler, ) { } @@ -22,9 +24,11 @@ class IndexController extends AbstractController #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(Request $request): Response { + $filterDto = $this->filterHandler->getFilterSettings(); + $query = $this ->applicationRepository - ->getPendingQuery() + ->getPendingQuery($filterDto); ; $pagination = $this->paginator->paginate( @@ -39,6 +43,7 @@ class IndexController extends AbstractController return $this->render('admin/application/index.html.twig', [ 'pagination' => $pagination, + 'filterDto' => $filterDto, ]); } } \ No newline at end of file diff --git a/src/Controller/Common/ApplicationFilterController.php b/src/Controller/Common/ApplicationFilterController.php new file mode 100644 index 0000000..3399006 --- /dev/null +++ b/src/Controller/Common/ApplicationFilterController.php @@ -0,0 +1,64 @@ +filterHandler->getFilterSettings(); + $filterOptions = $this->assignmentRepository->getFilterOptions(); + $formOptions = [ + 'min_date' => $filterOptions['minDate'], + 'max_date' => $filterOptions['maxDate'], + 'job_profiles' => $filterOptions['jobProfiles'], + ]; + + $form = $this->createForm(ApplicationFilterType::class, $formData, $formOptions); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->filterHandler->handleRequest($form); + $returnUrl = $this->getReturnUrl($request, 'app_admin_application_index'); + + return new HxRedirectResponse($returnUrl); + } + + return $this->render('common/modal_application_filter.html.twig', [ + 'filterForm' => $form->createView(), + 'filterDto' => $form->getData(), + ]); + } + + #[Route('/common/application/filter/reset', name: 'app_common_application_filter_reset')] + public function reset(Request $request): Response + { + $this->filterHandler->resetFilterSettings(); + $returnUrl = $this->getReturnUrl($request, 'app_admin_application_index'); + + return $this->redirect($returnUrl); + } +} \ No newline at end of file diff --git a/src/Form/ApplicationFilterType.php b/src/Form/ApplicationFilterType.php new file mode 100644 index 0000000..3308229 --- /dev/null +++ b/src/Form/ApplicationFilterType.php @@ -0,0 +1,130 @@ +destinations; + sort($destinations); + foreach ($destinations as $item) { + $this->hotelChoices[$item] = $item; + } + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('dateFrom', DatepickerType::class, [ + 'label' => 'Zeitraum von', + 'required' => false, + 'min_date' => $options['min_date'], + 'max_date' => $options['max_date'], + 'attr' => [ + 'placeholder' => 'nicht filtern', + ], + ]) + ->add('dateTo', DatepickerType::class, [ + 'label' => 'Zeitraum bis', + 'required' => false, + 'min_date' => $options['min_date'], + 'max_date' => $options['max_date'], + 'attr' => [ + 'placeholder' => 'nicht filtern', + ], + ]) + ->add('duration', ChoiceType::class, [ + 'label' => 'Einsatzdauer', + 'required' => false, + 'placeholder' => 'nicht filtern', + 'choices' => [ + 'Wochenende (2-4 Tage)' => ApplicationFilterDto::DURATION_WEEKEND, + 'Midweek (5-6 Tage)' => ApplicationFilterDto::DURATION_MID_WEEK, + 'Ganze Woche (7 Tage)' => ApplicationFilterDto::DURATION_FULL_WEEK, + 'Mehr als einen Woche' => ApplicationFilterDto::DURATION_MORE, + ], + ]) + ->add('includePast', CheckboxType::class, [ + 'label' => 'vergangene Einsätze anzeigen', + 'required' => false, + ]) + ->add('apply', SubmitType::class, [ + 'label' => 'filtern', + ]) + ->add('reset', SubmitType::class, [ + 'label' => 'reset', + ]) + ; + + if ($this->security->isGranted('ROLE_ADMINISTRATIVE')) { + $builder + ->add('id', IntegerType::class, [ + 'label' => 'ID', + 'required' => false, + ]) + ->add('status', MultiselectType::class, [ + 'label' => 'Status', + 'required' => false, + 'empty_label' => 'nicht filtern', + 'choices' => [ + 'voll besetzt' => Assignment::STATUS_STAFFED, + 'teilweise besetzt' => Assignment::STATUS_PARTLY_STAFFED, + 'unbesetzt' => Assignment::STATUS_UNSTAFFED, + 'mit Bewerbungen' => Assignment::STATUS_STAFFING, + ], + ]) + ; + } + + if (0 < count($options['job_profiles'])) { + $builder + ->add('jobProfiles', MultiselectEntityType::class, [ + 'label' => 'Job-Profil', + 'class' => JobProfile::class, + 'required' => false, + 'empty_label' => 'nicht filtern', + 'choice_label' => 'name', + 'choices' => $options['job_profiles'], + ]) + ; + } + + $builder + ->add('hotels', MultiselectType::class, [ + 'label' => 'Haus/Destination', + 'required' => false, + 'empty_label' => 'nicht filtern', + 'choices' => $this->hotelChoices, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => ApplicationFilterDto::class, + 'min_date' => null, + 'max_date' => null, + 'job_profiles' => [], + ]) + ->setAllowedTypes('min_date', [\DateTimeImmutable::class, 'null']) + ->setAllowedTypes('max_date', [\DateTimeImmutable::class, 'null']) + ; + } +} \ No newline at end of file diff --git a/src/Model/ApplicationFilterDto.php b/src/Model/ApplicationFilterDto.php new file mode 100644 index 0000000..07fb30e --- /dev/null +++ b/src/Model/ApplicationFilterDto.php @@ -0,0 +1,116 @@ +id; + } + + public function setId(?int $id): static + { + $this->id = $id; + + return $this; + } + + public function getDateFrom(): ?\DateTimeImmutable + { + return $this->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 getJobProfiles(): array + { + return $this->jobProfiles; + } + + public function setJobProfiles(array $jobProfiles): static + { + $this->jobProfiles = $jobProfiles; + + return $this; + } + + public function getHotels(): ?array + { + return $this->hotels; + } + + public function setHotels(array $hotels): static + { + $this->hotels = $hotels; + + return $this; + } + + public function getDuration(): ?int + { + return $this->duration; + } + + public function setDuration(?int $duration): static + { + $this->duration = $duration; + + return $this; + } + + public function getStatus(): array + { + return $this->status; + } + + public function setStatus(array $status): static + { + $this->status = $status; + + return $this; + } + + public function isIncludePast(): bool + { + return $this->includePast; + } + + public function setIncludePast(bool $includePast): static + { + $this->includePast = $includePast; + + return $this; + } +} \ No newline at end of file diff --git a/src/Repository/ApplicationRepository.php b/src/Repository/ApplicationRepository.php index 7286619..4700089 100644 --- a/src/Repository/ApplicationRepository.php +++ b/src/Repository/ApplicationRepository.php @@ -5,6 +5,8 @@ namespace App\Repository; use App\Entity\Application; use App\Entity\Assignment; use App\Entity\Teamer; +use App\Model\ApplicationFilterDto; +use App\Model\AssignmentFilterDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; @@ -24,11 +26,11 @@ class ApplicationRepository extends ServiceEntityRepository parent::__construct($registry, Application::class); } - public function getPendingQuery(): Query + public function getPendingQuery(ApplicationFilterDto $filterDto): Query { $qb = $this->createQueryBuilder('application'); - return $qb + $qb ->select('application', 'assignment', 'destination', 'job_profile', 'teamer') ->innerJoin('application.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') @@ -36,8 +38,70 @@ class ApplicationRepository extends ServiceEntityRepository ->innerJoin('application.teamer', 'teamer') ->where($qb->expr()->neq('application.status', ':status')) ->setParameter('status', Application::STATUS_REJECTED) - ->getQuery() ; + + if (false === $filterDto->isIncludePast()) { + $qb + ->andWhere($qb->expr()->gte('destination.dateFrom', ':now')) + ->setParameter('now', new \DateTimeImmutable()) + ; + } + if (null !== $dateFrom = $filterDto->getDateFrom()) { + $qb + ->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 ($jobProfiles = $filterDto->getJobProfiles()) { + $qb + ->andWhere($qb->expr()->in('assignment.jobProfile', ':jobProfiles')) + ->setParameter('jobProfiles', $jobProfiles) + ; + } + + if (0 < count($filterDto->getHotels())) { + $constraints = []; + foreach ($filterDto->getHotels() as $index => $hotel) { + $constraints[] = $qb->expr()->like('destination.hotel', ':hotel'.$index); + } + $qb->andWhere($qb->expr()->orX(...$constraints)); + foreach ($filterDto->getHotels() as $index => $hotel) { + $qb->setParameter('hotel'.$index, '%'.$hotel.'%'); + } + } + + 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 getPendingForTeamerQuery(Teamer $teamer): Query diff --git a/src/Service/Common/ApplicationFilterHandler.php b/src/Service/Common/ApplicationFilterHandler.php new file mode 100644 index 0000000..1212ced --- /dev/null +++ b/src/Service/Common/ApplicationFilterHandler.php @@ -0,0 +1,70 @@ +getSession()->get($this->namespace)) { + return new ApplicationFilterDto(); + } + + $filterDto = new ApplicationFilterDto(); + + if (isset($data['id'])) { + $filterDto->setId($data['id']); + } + if (isset($data['date_from'])) { + $filterDto->setDateFrom($data['date_from']); + } + if (isset($data['date_to'])) { + $filterDto->setDateTo($data['date_to']); + } + if (isset($data['job_profiles']) && 0 < count($data['job_profiles'])) { + $jobProfiles = $this + ->entityManager + ->getRepository(JobProfile::class) + ->findBy(['id' => $data['job_profiles']]) + ; + $filterDto->setJobProfiles($jobProfiles); + } + if (isset($data['hotels']) && 0 < count($data['hotels'])) { + $filterDto->setHotels($data['hotels']); + } + if (isset($data['duration']) && 0 < (int) $data['duration']) { + $filterDto->setDuration($data['duration']); + } + if (isset($data['status'])) { + $filterDto->setStatus($data['status']); + } + if (isset($data['include_past'])) { + $filterDto->setIncludePast((bool) $data['include_past']); + } + + return $filterDto; + } + + protected function saveFilterSettings(AbstractFilterDto $filterDto): void + { + /** @var ApplicationFilterDto $filterDto */ + $this->getSession()->set($this->namespace, [ + 'id' => $filterDto->getId(), + 'date_from' => $filterDto->getDateFrom(), + 'date_to' => $filterDto->getDateTo(), + 'job_profiles' => array_map(function (JobProfile $jobProfile) { + return $jobProfile->getId(); + }, $filterDto->getJobProfiles()), + 'hotels' => $filterDto->getHotels(), + 'duration' => $filterDto->getDuration(), + 'status' => $filterDto->getStatus(), + 'include_past' => $filterDto->isIncludePast(), + ]); + } +} \ No newline at end of file diff --git a/templates/admin/application/index.html.twig b/templates/admin/application/index.html.twig index 0a63c21..7c0694f 100644 --- a/templates/admin/application/index.html.twig +++ b/templates/admin/application/index.html.twig @@ -3,14 +3,40 @@ {% block title %}Bewerbungsübersicht{% endblock %} {% block content %} -