From e0ce495b860549d88aa0166ed044e1e60d709ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 22 Jul 2025 17:42:06 +0200 Subject: [PATCH] feat: filtering of custom destinations by hotel --- .../System/Destination/IndexController.php | 7 ++- .../Common/DestinationFilterController.php | 56 +++++++++++++++++++ src/Form/DestinationFilterType.php | 50 +++++++++++++++++ src/Model/DestinationFilterDto.php | 22 ++++++++ src/Repository/DestinationRepository.php | 26 +++++++++ .../Common/DestinationFilterHandler.php | 37 ++++++++++++ .../system/destination/index.html.twig | 20 +++++++ .../common/modal_destination_filter.html.twig | 16 ++++++ 8 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/Controller/Common/DestinationFilterController.php create mode 100644 src/Form/DestinationFilterType.php create mode 100644 src/Model/DestinationFilterDto.php create mode 100644 src/Service/Common/DestinationFilterHandler.php create mode 100644 templates/common/modal_destination_filter.html.twig diff --git a/src/Controller/Administrative/System/Destination/IndexController.php b/src/Controller/Administrative/System/Destination/IndexController.php index 3fbdd62..3a6faf6 100644 --- a/src/Controller/Administrative/System/Destination/IndexController.php +++ b/src/Controller/Administrative/System/Destination/IndexController.php @@ -3,6 +3,7 @@ namespace App\Controller\Administrative\System\Destination; use App\Repository\DestinationRepository; +use App\Service\Common\DestinationFilterHandler; 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 DestinationRepository $destinationRepository, private readonly PaginatorInterface $paginator, + private readonly DestinationFilterHandler $filterHandler ) { } @@ -22,9 +24,11 @@ class IndexController extends AbstractController #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(Request $request): Response { + $filterDto = $this->filterHandler->getFilterSettings(); + $query = $this ->destinationRepository - ->getListQueryForCustom() + ->getListQuery($filterDto) ; $pagination = $this->paginator->paginate( @@ -39,6 +43,7 @@ class IndexController extends AbstractController return $this->render('administrative/system/destination/index.html.twig', [ 'pagination' => $pagination, + 'filterDto' => $filterDto, ]); } } \ No newline at end of file diff --git a/src/Controller/Common/DestinationFilterController.php b/src/Controller/Common/DestinationFilterController.php new file mode 100644 index 0000000..c6c940e --- /dev/null +++ b/src/Controller/Common/DestinationFilterController.php @@ -0,0 +1,56 @@ +filterHandler->getFilterSettings(); + $form = $this->createForm(DestinationFilterType::class, $formData); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->filterHandler->handleRequest($form); + $returnUrl = $this->getReturnUrl($request, 'app_administrative_system_destination_index'); + + return new HxRedirectResponse($returnUrl); + } + + return $this->render('common/modal_destination_filter.html.twig', [ + 'filterForm' => $form->createView(), + 'filterDto' => $form->getData(), + ]); + } + + #[Route('/common/destination/filter/reset', name: 'app_common_destination_filter_reset')] + #[IsGranted('ROLE_ADMINISTRATIVE')] + public function reset(Request $request): Response + { + $this->filterHandler->resetFilterSettings(); + $returnUrl = $this->getReturnUrl($request, 'app_administrative_system_destination_index'); + + return $this->redirect($returnUrl); + } +} \ No newline at end of file diff --git a/src/Form/DestinationFilterType.php b/src/Form/DestinationFilterType.php new file mode 100644 index 0000000..af299f6 --- /dev/null +++ b/src/Form/DestinationFilterType.php @@ -0,0 +1,50 @@ +destinations; + sort($destinations); + foreach ($destinations as $item) { + $this->hotelChoices[$item] = $item; + } + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('hotels', MultiselectType::class, [ + 'label' => 'Haus/Destination', + 'required' => false, + 'empty_label' => 'nicht filtern', + 'choices' => $this->hotelChoices, + ]) + ->add('apply', SubmitType::class, [ + 'label' => 'filtern', + ]) + ->add('reset', SubmitType::class, [ + 'label' => 'reset', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => DestinationFilterDto::class, + ]); + } +} \ No newline at end of file diff --git a/src/Model/DestinationFilterDto.php b/src/Model/DestinationFilterDto.php new file mode 100644 index 0000000..f5c5ffd --- /dev/null +++ b/src/Model/DestinationFilterDto.php @@ -0,0 +1,22 @@ +hotels; + } + + public function setHotels(array $hotels): static + { + $this->hotels = $hotels; + + return $this; + } +} \ No newline at end of file diff --git a/src/Repository/DestinationRepository.php b/src/Repository/DestinationRepository.php index 83f6d2d..95bd23a 100644 --- a/src/Repository/DestinationRepository.php +++ b/src/Repository/DestinationRepository.php @@ -3,6 +3,7 @@ namespace App\Repository; use App\Entity\Destination; +use App\Model\DestinationFilterDto; use App\Repository\Traits\QueryHelperTrait; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; @@ -25,6 +26,31 @@ class DestinationRepository extends ServiceEntityRepository parent::__construct($registry, Destination::class); } + public function getListQuery(DestinationFilterDto $filterDto): Query + { + $qb = $this->createQueryBuilder('destination'); + + $qb->where( + $qb->expr()->andX( + $qb->expr()->isNull('destination.deletedAt'), + $qb->expr()->isNull('destination.busProId') + ) + ); + + 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.'%'); + } + } + + return $qb->getQuery(); + } + public function getListQueryForCustom(): Query { $qb = $this->createQueryBuilder('destination'); diff --git a/src/Service/Common/DestinationFilterHandler.php b/src/Service/Common/DestinationFilterHandler.php new file mode 100644 index 0000000..c06b34f --- /dev/null +++ b/src/Service/Common/DestinationFilterHandler.php @@ -0,0 +1,37 @@ +modelClass(); + + if (null === $data = $this->getSession()->get($this->namespace)) { + return $filterDto; + } + + if (isset($data['hotels']) && 0 < count($data['hotels'])) { + $filterDto->setHotels($data['hotels']); + } + + return $filterDto; + } + + protected function saveFilterSettings(AbstractFilterDto $filterDto): void + { + /** @var DestinationFilterDto $filterDto */ + $this->getSession()->set($this->namespace, [ + 'hotels' => $filterDto->getHotels(), + ]); + } +} \ No newline at end of file diff --git a/templates/administrative/system/destination/index.html.twig b/templates/administrative/system/destination/index.html.twig index db133b6..e234423 100644 --- a/templates/administrative/system/destination/index.html.twig +++ b/templates/administrative/system/destination/index.html.twig @@ -7,6 +7,26 @@

Manuell angelegte Reisen

+
+ + {% if filterDto.active %} + + Reset + + {% endif %} +
diff --git a/templates/common/modal_destination_filter.html.twig b/templates/common/modal_destination_filter.html.twig new file mode 100644 index 0000000..3e00b31 --- /dev/null +++ b/templates/common/modal_destination_filter.html.twig @@ -0,0 +1,16 @@ +{% extends 'htmx_modal.html.twig' %} + +{% block title %}Destinationen filtern{% endblock %} + +{% block content %} + {{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }} +
+ {{ form_row(filterForm.hotels) }} +
+
+ {{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn' } }) }} + {{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--secondary' } }) }} +
+ {{ form_rest(filterForm) }} + {{ form_end(filterForm) }} +{% endblock %} \ No newline at end of file