From a35f91ac631be8a8b7d52b9d1a129aefaaa7e6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 27 Oct 2026 17:02:51 +0100 Subject: [PATCH] feat: list and show submitted questionnaires to the office --- .../Questionnaire/DetailController.php | 43 ++++++++ .../Questionnaire/IndexController.php | 51 ++++++++++ .../QuestionnaireResponseFilterController.php | 48 +++++++++ src/Form/QuestionnaireResponseFilterType.php | 53 ++++++++++ src/Menu/AdminMenuBuilder.php | 12 +++ src/Menu/ManagerMenuBuilder.php | 12 +++ src/Model/QuestionnaireResponseFilterDto.php | 61 ++++++++++++ .../QuestionnaireResponseRepository.php | 58 +++++++++++ .../QuestionnaireResponseFilterHandler.php | 48 +++++++++ .../questionnaire/detail.html.twig | 45 +++++++++ .../questionnaire/index.html.twig | 96 ++++++++++++++++++ ...al_questionnaire_response_filter.html.twig | 19 ++++ ...DispositionRepositoryQuestionnaireTest.php | 48 +-------- .../Repository/QuestionnaireFixturesTrait.php | 68 +++++++++++++ .../QuestionnaireResponseRepositoryTest.php | 99 +++++++++++++++++++ 15 files changed, 715 insertions(+), 46 deletions(-) create mode 100644 src/Controller/Administrative/Questionnaire/DetailController.php create mode 100644 src/Controller/Administrative/Questionnaire/IndexController.php create mode 100644 src/Controller/Common/QuestionnaireResponseFilterController.php create mode 100644 src/Form/QuestionnaireResponseFilterType.php create mode 100644 src/Model/QuestionnaireResponseFilterDto.php create mode 100644 src/Service/Common/QuestionnaireResponseFilterHandler.php create mode 100644 templates/administrative/questionnaire/detail.html.twig create mode 100644 templates/administrative/questionnaire/index.html.twig create mode 100644 templates/common/modal_questionnaire_response_filter.html.twig create mode 100644 tests/Repository/QuestionnaireFixturesTrait.php create mode 100644 tests/Repository/QuestionnaireResponseRepositoryTest.php diff --git a/src/Controller/Administrative/Questionnaire/DetailController.php b/src/Controller/Administrative/Questionnaire/DetailController.php new file mode 100644 index 0000000..a81577e --- /dev/null +++ b/src/Controller/Administrative/Questionnaire/DetailController.php @@ -0,0 +1,43 @@ +isSubmitted()) { + throw new NotFoundHttpException(); + } + + return $this->render('administrative/questionnaire/detail.html.twig', [ + 'definition' => $this->catalog->get($response->getQuestionnaire(), $response->getVersion()), + 'response' => $response, + 'returnUrl' => $this->getReturnUrl($request, 'app_administrative_questionnaire_index'), + ]); + } +} diff --git a/src/Controller/Administrative/Questionnaire/IndexController.php b/src/Controller/Administrative/Questionnaire/IndexController.php new file mode 100644 index 0000000..ed48633 --- /dev/null +++ b/src/Controller/Administrative/Questionnaire/IndexController.php @@ -0,0 +1,51 @@ +filterHandler->getFilterSettings(); + + $pagination = $this->paginator->paginate( + $this->responseRepository->getListQuery(QuestionnaireCatalog::TEAMER_ESCORT, $filterDto), + $request->query->getInt('page', 1), + $request->query->getInt('limit', 50), + [ + 'defaultSortFieldName' => 'response.submittedAt', + 'defaultSortDirection' => 'desc', + ] + ); + + return $this->render('administrative/questionnaire/index.html.twig', [ + 'definition' => $this->catalog->get(QuestionnaireCatalog::TEAMER_ESCORT), + 'pagination' => $pagination, + 'filterDto' => $filterDto, + ]); + } +} diff --git a/src/Controller/Common/QuestionnaireResponseFilterController.php b/src/Controller/Common/QuestionnaireResponseFilterController.php new file mode 100644 index 0000000..2a4969e --- /dev/null +++ b/src/Controller/Common/QuestionnaireResponseFilterController.php @@ -0,0 +1,48 @@ +filterHandler->getFilterSettings(); + $form = $this->createForm(QuestionnaireResponseFilterType::class, $formData); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->filterHandler->handleRequest($form); + + return new HxRedirectResponse($this->generateUrl('app_administrative_questionnaire_index')); + } + + return $this->render('common/modal_questionnaire_response_filter.html.twig', [ + 'filterForm' => $form->createView(), + 'filterDto' => $form->getData(), + ]); + } + + #[Route('/common/questionnaire-response/filter/reset', name: 'app_common_questionnaire_response_filter_reset')] + #[IsGranted('ROLE_ADMINISTRATIVE')] + public function reset(): Response + { + $this->filterHandler->resetFilterSettings(); + + return $this->redirectToRoute('app_administrative_questionnaire_index'); + } +} diff --git a/src/Form/QuestionnaireResponseFilterType.php b/src/Form/QuestionnaireResponseFilterType.php new file mode 100644 index 0000000..1d4f86e --- /dev/null +++ b/src/Form/QuestionnaireResponseFilterType.php @@ -0,0 +1,53 @@ +add('teamer', AutocompleteEntityType::class, [ + 'label' => 'Teamer', + 'required' => false, + 'class' => Teamer::class, + 'label_property' => 'name', + 'label_function' => fn (Teamer $teamer) => (string) $teamer, + 'endpoint_route' => 'app_admin_autocomplete_teamer', + ]) + ->add('hotel', TextType::class, [ + 'label' => 'Haus', + 'required' => false, + ]) + ->add('dateFrom', DatepickerType::class, [ + 'label' => 'Einsatzende von', + 'required' => false, + ]) + ->add('dateTo', DatepickerType::class, [ + 'label' => 'Einsatzende bis', + 'required' => false, + ]) + ->add('apply', SubmitType::class, [ + 'label' => 'filtern', + ]) + ->add('reset', SubmitType::class, [ + 'label' => 'reset', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => QuestionnaireResponseFilterDto::class, + ]); + } +} diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index bbbd8ca..3817f42 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -109,6 +109,18 @@ class AdminMenuBuilder extends AbstractMenuBuilder ], ], ]); + $menu->addChild('Fragebögen', [ + 'route' => 'app_administrative_questionnaire_index', + 'linkAttributes' => [ + 'title' => 'Fragebögen der Busbegleitungen', + ], + 'extras' => [ + 'icon' => 'bus', + 'routes' => [ + ['pattern' => '/^app_administrative_questionnaire_/'], + ], + ], + ]); $menu->addChild('Teamübersicht', [ 'route' => 'app_administrative_teamer_index', diff --git a/src/Menu/ManagerMenuBuilder.php b/src/Menu/ManagerMenuBuilder.php index 783ca3e..d9f9218 100644 --- a/src/Menu/ManagerMenuBuilder.php +++ b/src/Menu/ManagerMenuBuilder.php @@ -118,6 +118,18 @@ class ManagerMenuBuilder extends AbstractMenuBuilder ], ], ]); + $menu->addChild('Fragebögen', [ + 'route' => 'app_administrative_questionnaire_index', + 'linkAttributes' => [ + 'title' => 'Fragebögen der Busbegleitungen', + ], + 'extras' => [ + 'icon' => 'bus', + 'routes' => [ + ['pattern' => '/^app_administrative_questionnaire_/'], + ], + ], + ]); $statisticsMenu = $menu->addChild('Statistiken', [ 'extras' => [ 'icon' => 'chart', diff --git a/src/Model/QuestionnaireResponseFilterDto.php b/src/Model/QuestionnaireResponseFilterDto.php new file mode 100644 index 0000000..69a3749 --- /dev/null +++ b/src/Model/QuestionnaireResponseFilterDto.php @@ -0,0 +1,61 @@ +teamer; + } + + public function setTeamer(?Teamer $teamer): static + { + $this->teamer = $teamer; + + return $this; + } + + public function getHotel(): ?string + { + return $this->hotel; + } + + public function setHotel(?string $hotel): static + { + $this->hotel = $hotel; + + 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; + } +} diff --git a/src/Repository/QuestionnaireResponseRepository.php b/src/Repository/QuestionnaireResponseRepository.php index 8e027cd..46b0f73 100644 --- a/src/Repository/QuestionnaireResponseRepository.php +++ b/src/Repository/QuestionnaireResponseRepository.php @@ -4,7 +4,11 @@ namespace App\Repository; use App\Entity\Disposition; use App\Entity\QuestionnaireResponse; +use App\Enum\QuestionnaireResponseStatus; +use App\Model\QuestionnaireResponseFilterDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; /** @@ -22,6 +26,60 @@ class QuestionnaireResponseRepository extends ServiceEntityRepository parent::__construct($registry, QuestionnaireResponse::class); } + /** + * Submitted responses to one questionnaire for the office's list. Drafts stay out: until the + * teamer submits, a response is theirs. Filtered and sorted on the live disposition rather + * than the snapshot in `context`, which JSON would make neither sortable nor indexable. + */ + public function getListQuery(string $questionnaire, QuestionnaireResponseFilterDto $filterDto): Query + { + $qb = $this->createQueryBuilder('response'); + + $qb + ->select('response', 'disposition', 'teamer', 'assignment', 'destination') + ->innerJoin('response.disposition', 'disposition') + ->innerJoin('disposition.teamer', 'teamer') + ->innerJoin('disposition.assignment', 'assignment') + ->innerJoin('assignment.destination', 'destination') + ->where($qb->expr()->andX( + $qb->expr()->eq('response.questionnaire', ':questionnaire'), + $qb->expr()->eq('response.status', ':submitted') + )) + ->setParameter('questionnaire', $questionnaire) + ->setParameter('submitted', QuestionnaireResponseStatus::SUBMITTED) + ; + + if (null !== $teamer = $filterDto->getTeamer()) { + $qb + ->andWhere($qb->expr()->eq('disposition.teamer', ':teamer')) + ->setParameter('teamer', $teamer) + ; + } + + if (null !== $hotel = $filterDto->getHotel()) { + $qb + ->andWhere($qb->expr()->like('destination.hotel', ':hotel')) + ->setParameter('hotel', '%'.addcslashes($hotel, '%_').'%') + ; + } + + if (null !== $dateFrom = $filterDto->getDateFrom()) { + $qb + ->andWhere($qb->expr()->gte('destination.dateTo', ':dateFrom')) + ->setParameter('dateFrom', $dateFrom, Types::DATE_IMMUTABLE) + ; + } + + if (null !== $dateTo = $filterDto->getDateTo()) { + $qb + ->andWhere($qb->expr()->lte('destination.dateTo', ':dateTo')) + ->setParameter('dateTo', $dateTo, Types::DATE_IMMUTABLE) + ; + } + + return $qb->getQuery(); + } + public function findOneByDisposition(string $questionnaire, Disposition $disposition): ?QuestionnaireResponse { return $this->findOneBy(['questionnaire' => $questionnaire, 'disposition' => $disposition]); diff --git a/src/Service/Common/QuestionnaireResponseFilterHandler.php b/src/Service/Common/QuestionnaireResponseFilterHandler.php new file mode 100644 index 0000000..662b7c2 --- /dev/null +++ b/src/Service/Common/QuestionnaireResponseFilterHandler.php @@ -0,0 +1,48 @@ +modelClass(); + + if (null === $data = $this->getSession()->get($this->namespace)) { + return $filterDto; + } + + if (isset($data['teamer_id'])) { + $filterDto->setTeamer($this->entityManager->getRepository(Teamer::class)->find($data['teamer_id'])); + } + if (isset($data['hotel'])) { + $filterDto->setHotel($data['hotel']); + } + if (isset($data['date_from'])) { + $filterDto->setDateFrom($data['date_from']); + } + if (isset($data['date_to'])) { + $filterDto->setDateTo($data['date_to']); + } + + return $filterDto; + } + + protected function saveFilterSettings(AbstractFilterDto $filterDto): void + { + /* @var QuestionnaireResponseFilterDto $filterDto */ + $this->getSession()->set($this->namespace, [ + 'teamer_id' => $filterDto->getTeamer()?->getId(), + 'hotel' => $filterDto->getHotel(), + 'date_from' => $filterDto->getDateFrom(), + 'date_to' => $filterDto->getDateTo(), + ]); + } +} diff --git a/templates/administrative/questionnaire/detail.html.twig b/templates/administrative/questionnaire/detail.html.twig new file mode 100644 index 0000000..485d39b --- /dev/null +++ b/templates/administrative/questionnaire/detail.html.twig @@ -0,0 +1,45 @@ +{% extends 'administrative/layout.html.twig' %} + +{% block title %}{{ definition.title }}{% endblock %} + +{% block content %} + {% set context = response.context %} +

+ {{ definition.title }} +

+

+ {{ context.teamerName|default('-') }}, abgeschickt am {{ response.submittedAt|date('d.m.Y, H:i') }} Uhr +

+
+
+ {% include 'questionnaire/_answers.html.twig' %} +
+
+ {# the snapshot taken on submit, so it shows the placement as the teamer answered for it #} +

+ Einsatz +

+
+ {% for label, value in { + 'Teamer:in': context.teamerName|default(null), + 'Jobprofil': context.jobProfileName|default(null), + 'Destination': context.destinationProduct|default(null), + 'Haus': context.hotel|default(null) ? context.hotel ~ ' (' ~ context.hotelCode ~ ')' : null, + 'Einsatztermin': context.dateFrom|default(null) ? context.dateFrom|date('d.m.Y') ~ ' - ' ~ context.dateTo|date('d.m.Y') : null, + 'Busbegleitung ab': context.escortPickup|default(null) ? context.escortPickup.city ~ ', ' ~ context.escortPickup.street ~ (context.escortPickup.time ? ', ' ~ context.escortPickup.time ~ ' Uhr' : '') : null, + 'Abfahrt Hinfahrt': context.escortPickupDate|default(null) ? context.escortPickupDate|date('d.m.Y') : null, + } %} +
+
{{ label }}
+
{{ (value ?? '-') }}
+
+ {% endfor %} +
+ +
+
+{% endblock %} diff --git a/templates/administrative/questionnaire/index.html.twig b/templates/administrative/questionnaire/index.html.twig new file mode 100644 index 0000000..14ac90f --- /dev/null +++ b/templates/administrative/questionnaire/index.html.twig @@ -0,0 +1,96 @@ +{% extends 'administrative/layout.html.twig' %} + +{% block title %}Fragebögen{% endblock %} + +{% block content %} +
+

+ Fragebögen: {{ definition.title }} +

+
+ + {% if filterDto.active %} + + Reset + + {% endif %} +
+
+ +
+
+ + + + + + + + + + + + + {% for response in pagination %} + {% set disposition = response.disposition %} + {% set destination = disposition.assignment.destination %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
+ {{ knp_pagination_sortable(pagination, 'Name', 'teamer.lastName') }} + + {{ knp_pagination_sortable(pagination, 'Vorname', 'teamer.firstName') }} + + {{ knp_pagination_sortable(pagination, 'Einsatzzeitraum', 'destination.dateFrom') }} + + {{ knp_pagination_sortable(pagination, 'Haus', 'destination.hotel') }} + + {{ knp_pagination_sortable(pagination, 'Abgeschickt', 'response.submittedAt') }} +
+ {{ disposition.teamer.lastName }} + + {{ disposition.teamer.firstName }} + {% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %} + + {{ destination.dateFrom|date('d.m.Y') }}- +
+ {{ destination.dateTo|date('d.m.Y') }} +
+ {{ destination.hotel }} + + {{ response.submittedAt|date('d.m.Y') }} + + + {{ icon('info', 'w-4 h-4') }} + +
+ Keine Daten... +
+ {{ knp_pagination_render(pagination) }} +
+
+{% endblock %} diff --git a/templates/common/modal_questionnaire_response_filter.html.twig b/templates/common/modal_questionnaire_response_filter.html.twig new file mode 100644 index 0000000..76c82d6 --- /dev/null +++ b/templates/common/modal_questionnaire_response_filter.html.twig @@ -0,0 +1,19 @@ +{% extends 'htmx_modal.html.twig' %} + +{% block title %}Fragebögen filtern{% endblock %} + +{% block content %} + {{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }} +
+ {{ form_row(filterForm.teamer) }} + {{ form_row(filterForm.hotel) }} + {{ form_row(filterForm.dateFrom) }} + {{ form_row(filterForm.dateTo) }} +
+
+ {{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn' } }) }} + {{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--secondary' } }) }} +
+ {{ form_rest(filterForm) }} + {{ form_end(filterForm) }} +{% endblock %} diff --git a/tests/Repository/DispositionRepositoryQuestionnaireTest.php b/tests/Repository/DispositionRepositoryQuestionnaireTest.php index 9c79c22..cd6cc83 100644 --- a/tests/Repository/DispositionRepositoryQuestionnaireTest.php +++ b/tests/Repository/DispositionRepositoryQuestionnaireTest.php @@ -4,14 +4,9 @@ declare(strict_types=1); namespace App\Tests\Repository; -use App\Config\QuestionnaireCatalog; -use App\Entity\Application; use App\Entity\Assignment; -use App\Entity\Destination; use App\Entity\Disposition; -use App\Entity\QuestionnaireResponse; use App\Entity\Teamer; -use App\Model\Questionnaire\Definition; use App\Repository\DispositionRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -27,6 +22,8 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; */ class DispositionRepositoryQuestionnaireTest extends KernelTestCase { + use QuestionnaireFixturesTrait; + private const string GO_LIVE = '2026-01-01'; private EntityManagerInterface $entityManager; @@ -138,45 +135,4 @@ class DispositionRepositoryQuestionnaireTest extends KernelTestCase return $this->repository->findPendingQuestionnaireByTeamer($this->teamer, new \DateTimeImmutable(self::GO_LIVE)); } - - private function escort(string $dateTo = 'yesterday', ?Teamer $teamer = null): Disposition - { - $destination = (new Destination()) - ->setProduct('Davos Klosters - Schwendi') - ->setHotel('Schwendi') - ->setHotelCode('DKI') - ->setHotelBusProId(1) - ->setDateFrom(new \DateTimeImmutable($dateTo.' -7 days')) - ->setDateTo(new \DateTimeImmutable($dateTo)) - ->setPickups([['busProId' => 4711, 'city' => 'Stuttgart', 'street' => 'Busbahnhof', 'time' => '21:30']]) - ; - $assignment = (new Assignment()) - ->setDestination($destination) - ->setPickup(4711) - ->setStatus(Assignment::STATUS_STAFFED) - ; - $disposition = (new Disposition(new Application($assignment, $teamer ?? $this->teamer))) - ->setStatus(Disposition::STATUS_ENDED) - ; - - $this->entityManager->persist($destination); - $this->entityManager->persist($assignment); - $this->entityManager->persist($disposition); - - return $disposition; - } - - private function respond(Disposition $disposition, bool $submit, string $questionnaire = QuestionnaireCatalog::TEAMER_ESCORT): void - { - $definition = new Definition($questionnaire, 1, 'Beispiel', null, []); - $response = QuestionnaireResponse::forDisposition($definition, $disposition); - - if ($submit) { - $response->submit($definition, [], [], new \DateTimeImmutable()); - } else { - $response->saveDraft([], []); - } - - $this->entityManager->persist($response); - } } diff --git a/tests/Repository/QuestionnaireFixturesTrait.php b/tests/Repository/QuestionnaireFixturesTrait.php new file mode 100644 index 0000000..44d0073 --- /dev/null +++ b/tests/Repository/QuestionnaireFixturesTrait.php @@ -0,0 +1,68 @@ +setProduct('Davos Klosters - Schwendi') + ->setHotel($hotel) + ->setHotelCode('DKI') + ->setHotelBusProId(1) + ->setDateFrom(new \DateTimeImmutable($dateTo.' -7 days')) + ->setDateTo(new \DateTimeImmutable($dateTo)) + ->setPickups([['busProId' => 4711, 'city' => 'Stuttgart', 'street' => 'Busbahnhof', 'time' => '21:30']]) + ; + $assignment = (new Assignment()) + ->setDestination($destination) + ->setPickup(4711) + ->setStatus(Assignment::STATUS_STAFFED) + ; + $disposition = (new Disposition(new Application($assignment, $teamer ?? $this->teamer))) + ->setStatus(Disposition::STATUS_ENDED) + ; + + $this->entityManager->persist($destination); + $this->entityManager->persist($assignment); + $this->entityManager->persist($disposition); + + return $disposition; + } + + private function respond(Disposition $disposition, bool $submit, string $questionnaire = QuestionnaireCatalog::TEAMER_ESCORT): QuestionnaireResponse + { + $definition = new Definition($questionnaire, 1, 'Beispiel', null, []); + $response = QuestionnaireResponse::forDisposition($definition, $disposition); + + if ($submit) { + $response->submit($definition, [], [], new \DateTimeImmutable()); + } else { + $response->saveDraft([], []); + } + + $this->entityManager->persist($response); + + return $response; + } +} diff --git a/tests/Repository/QuestionnaireResponseRepositoryTest.php b/tests/Repository/QuestionnaireResponseRepositoryTest.php new file mode 100644 index 0000000..ec02b81 --- /dev/null +++ b/tests/Repository/QuestionnaireResponseRepositoryTest.php @@ -0,0 +1,99 @@ +entityManager = self::getContainer()->get(EntityManagerInterface::class); + $this->repository = $this->entityManager->getRepository(QuestionnaireResponse::class); + $this->entityManager->getConnection()->beginTransaction(); + + $this->teamer = (new Teamer())->setFirstName('Kim')->setLastName('Muster')->setGender('d'); + $this->entityManager->persist($this->teamer); + } + + protected function tearDown(): void + { + $this->entityManager->getConnection()->rollBack(); + parent::tearDown(); + } + + public function testOnlySubmittedResponsesToThisQuestionnaireAreListed(): void + { + $submitted = $this->respond($this->escort(), submit: true); + $this->respond($this->escort(), submit: false); + $this->respond($this->escort(), submit: true, questionnaire: 'other'); + + $this->assertSame([$submitted], $this->list(new QuestionnaireResponseFilterDto())); + } + + public function testFilterByTeamer(): void + { + $other = (new Teamer())->setFirstName('Alex')->setLastName('Beispiel')->setGender('d'); + $this->entityManager->persist($other); + $this->respond($this->escort(teamer: $other), submit: true); + $mine = $this->respond($this->escort(), submit: true); + + $this->assertSame([$mine], $this->list((new QuestionnaireResponseFilterDto())->setTeamer($this->teamer))); + } + + /** + * Matched as a substring, and a % typed into the filter is a literal, not a wildcard. + */ + public function testFilterByHotel(): void + { + $ranalt = $this->respond($this->escort(hotel: 'Gasthof Ranalt'), submit: true); + $this->respond($this->escort(hotel: 'Schwendi'), submit: true); + + $this->assertSame([$ranalt], $this->list((new QuestionnaireResponseFilterDto())->setHotel('ranalt'))); + $this->assertSame([], $this->list((new QuestionnaireResponseFilterDto())->setHotel('%'))); + } + + public function testFilterByEndOfAssignment(): void + { + $recent = $this->respond($this->escort(dateTo: '2026-03-14'), submit: true); + $this->respond($this->escort(dateTo: '2026-01-10'), submit: true); + + $filter = (new QuestionnaireResponseFilterDto()) + ->setDateFrom(new \DateTimeImmutable('2026-03-01')) + ->setDateTo(new \DateTimeImmutable('2026-03-14')) + ; + + $this->assertSame([$recent], $this->list($filter)); + } + + /** + * @return list + */ + private function list(QuestionnaireResponseFilterDto $filter): array + { + $this->entityManager->flush(); + + return $this->repository->getListQuery(QuestionnaireCatalog::TEAMER_ESCORT, $filter)->getResult(); + } +}