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 %} +
+ {{ context.teamerName|default('-') }}, abgeschickt am {{ response.submittedAt|date('d.m.Y, H:i') }} Uhr +
+| + {{ 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... + | +|||||