diff --git a/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php b/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php new file mode 100644 index 0000000..a839ceb --- /dev/null +++ b/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php @@ -0,0 +1,48 @@ +filterHandler->getFilterSettings(); + + $form = $this->createForm(StatisticsFilterType::class, $filterDto); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $filterDto = $this->filterHandler->handleRequest($form); + } + + $statistics = $this->dispositionRepository->getFeedbackStatisticsByNormalizedHotelCode( + $filterDto->getDateFrom(), + $filterDto->getDateTo(), + ); + + return $this->render('administrative/statistics/feedback_by_destination.html.twig', [ + 'form' => $form->createView(), + 'filterDto' => $filterDto, + 'statistics' => $statistics, + ]); + } +} diff --git a/src/Controller/Administrative/Statistics/FeedbackRatingsController.php b/src/Controller/Administrative/Statistics/FeedbackRatingsController.php index 27a1fc9..cafb676 100644 --- a/src/Controller/Administrative/Statistics/FeedbackRatingsController.php +++ b/src/Controller/Administrative/Statistics/FeedbackRatingsController.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace App\Controller\Administrative\Statistics; -use App\Form\FeedbackStatisticsFilterType; +use App\Form\StatisticsFilterType; use App\Repository\FeedbackRepository; -use App\Service\Common\FeedbackStatisticsFilterHandler; +use App\Service\Common\StatisticsFilterHandler; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -16,7 +16,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; class FeedbackRatingsController extends AbstractController { public function __construct( - private readonly FeedbackStatisticsFilterHandler $filterHandler, + private readonly StatisticsFilterHandler $filterHandler, private readonly FeedbackRepository $feedbackRepository, ) { } @@ -27,7 +27,7 @@ class FeedbackRatingsController extends AbstractController { $filterDto = $this->filterHandler->getFilterSettings(); - $form = $this->createForm(FeedbackStatisticsFilterType::class, $filterDto); + $form = $this->createForm(StatisticsFilterType::class, $filterDto); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Form/FeedbackStatisticsFilterType.php b/src/Form/StatisticsFilterType.php similarity index 86% rename from src/Form/FeedbackStatisticsFilterType.php rename to src/Form/StatisticsFilterType.php index 7ed5970..f265e81 100644 --- a/src/Form/FeedbackStatisticsFilterType.php +++ b/src/Form/StatisticsFilterType.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace App\Form; -use App\Model\FeedbackStatisticsFilterDto; +use App\Model\StatisticsFilterDto; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class FeedbackStatisticsFilterType extends AbstractType +class StatisticsFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { @@ -36,7 +36,7 @@ class FeedbackStatisticsFilterType extends AbstractType { $resolver ->setDefaults([ - 'data_class' => FeedbackStatisticsFilterDto::class, + 'data_class' => StatisticsFilterDto::class, ]) ; } diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index d493a87..01b9e6f 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -118,14 +118,25 @@ class AdminMenuBuilder extends AbstractMenuBuilder 'icon' => 'chart', ], ]); - $statisticsMenu->addChild('Feedback-Bewertungen', [ + $statisticsMenu->addChild('Bewertungen nach Kategorie', [ 'route' => 'app_administrative_statistics_feedback_ratings', 'linkAttributes' => [ - 'title' => 'Feedback-Bewertungen', + 'title' => 'Bewertungen nach Kategorie', ], 'extras' => [ 'routes' => [ - ['pattern' => '/^app_administrative_statistics_/'], + ['pattern' => '/^app_administrative_statistics_feedback_ratings/'], + ], + ], + ]); + $statisticsMenu->addChild('Feedback pro Destination', [ + 'route' => 'app_administrative_statistics_feedback_by_destination', + 'linkAttributes' => [ + 'title' => 'Feedback pro Destination', + ], + 'extras' => [ + 'routes' => [ + ['pattern' => '/^app_administrative_statistics_feedback_by_destination/'], ], ], ]); diff --git a/src/Menu/ManagerMenuBuilder.php b/src/Menu/ManagerMenuBuilder.php index efefaa1..99eab18 100644 --- a/src/Menu/ManagerMenuBuilder.php +++ b/src/Menu/ManagerMenuBuilder.php @@ -99,14 +99,25 @@ class ManagerMenuBuilder extends AbstractMenuBuilder 'icon' => 'chart', ], ]); - $statisticsMenu->addChild('Feedback-Bewertungen', [ + $statisticsMenu->addChild('Bewertungen nach Kategorie', [ 'route' => 'app_administrative_statistics_feedback_ratings', 'linkAttributes' => [ - 'title' => 'Feedback-Bewertungen', + 'title' => 'Bewertungen nach Kategorie', ], 'extras' => [ 'routes' => [ - ['pattern' => '/^app_administrative_statistics_/'], + ['pattern' => '/^app_administrative_statistics_feedback_ratings/'], + ], + ], + ]); + $statisticsMenu->addChild('Feedback pro Destination', [ + 'route' => 'app_administrative_statistics_feedback_by_destination', + 'linkAttributes' => [ + 'title' => 'Feedback pro Destination', + ], + 'extras' => [ + 'routes' => [ + ['pattern' => '/^app_administrative_statistics_feedback_by_destination/'], ], ], ]); diff --git a/src/Model/FeedbackStatisticsFilterDto.php b/src/Model/StatisticsFilterDto.php similarity index 91% rename from src/Model/FeedbackStatisticsFilterDto.php rename to src/Model/StatisticsFilterDto.php index a00368c..3eaead9 100644 --- a/src/Model/FeedbackStatisticsFilterDto.php +++ b/src/Model/StatisticsFilterDto.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Model; -class FeedbackStatisticsFilterDto extends AbstractFilterDto +class StatisticsFilterDto extends AbstractFilterDto { protected ?\DateTimeImmutable $dateFrom = null; protected ?\DateTimeImmutable $dateTo = null; diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index fcbd0c9..bf19601 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -318,7 +318,11 @@ class DispositionRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->in('disposition.status', ':statuses')) - ->setParameter('statuses', [Disposition::STATUS_ENDED, Disposition::STATUS_COMPLETED]) + ->setParameter('statuses', [ + Disposition::STATUS_ENDED, + Disposition::STATUS_PAID, + Disposition::STATUS_COMPLETED, + ]) ->groupBy('destination.hotelCode', 'destination.hotel') ->orderBy('destination.hotel', 'ASC') ; @@ -406,7 +410,11 @@ class DispositionRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->in('disposition.status', ':statuses')) - ->setParameter('statuses', [Disposition::STATUS_ENDED, Disposition::STATUS_COMPLETED]) + ->setParameter('statuses', [ + Disposition::STATUS_ENDED, + Disposition::STATUS_PAID, + Disposition::STATUS_COMPLETED, + ]) ->groupBy('hotelCode') ->orderBy('hotelCode', 'ASC') ; diff --git a/src/Service/Common/FeedbackStatisticsFilterHandler.php b/src/Service/Common/StatisticsFilterHandler.php similarity index 67% rename from src/Service/Common/FeedbackStatisticsFilterHandler.php rename to src/Service/Common/StatisticsFilterHandler.php index 790f375..e1c6168 100644 --- a/src/Service/Common/FeedbackStatisticsFilterHandler.php +++ b/src/Service/Common/StatisticsFilterHandler.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace App\Service\Common; use App\Model\AbstractFilterDto; -use App\Model\FeedbackStatisticsFilterDto; +use App\Model\StatisticsFilterDto; -class FeedbackStatisticsFilterHandler extends AbstractFilterHandler +class StatisticsFilterHandler extends AbstractFilterHandler { - protected string $namespace = 'filter:feedback_statistics'; - protected string $modelClass = FeedbackStatisticsFilterDto::class; + protected string $namespace = 'filter:statistics'; + protected string $modelClass = StatisticsFilterDto::class; - public function getFilterSettings(): FeedbackStatisticsFilterDto + public function getFilterSettings(): StatisticsFilterDto { $filterDto = new $this->modelClass(); @@ -32,7 +32,7 @@ class FeedbackStatisticsFilterHandler extends AbstractFilterHandler protected function saveFilterSettings(AbstractFilterDto $filterDto): void { - /* @var FeedbackStatisticsFilterDto $filterDto */ + /* @var StatisticsFilterDto $filterDto */ $this->getSession()->set($this->namespace, [ 'date_from' => $filterDto->getDateFrom(), 'date_to' => $filterDto->getDateTo(), diff --git a/templates/administrative/statistics/feedback_by_destination.html.twig b/templates/administrative/statistics/feedback_by_destination.html.twig new file mode 100644 index 0000000..22f770d --- /dev/null +++ b/templates/administrative/statistics/feedback_by_destination.html.twig @@ -0,0 +1,107 @@ +{% extends 'administrative/layout.html.twig' %} + +{% block title %}Feedback pro Destination{% endblock %} + +{% block content %} +
+

+ Feedback pro Destination +

+
+ +
+ {{ form_start(form, { attr: { class: 'flex flex-wrap items-end gap-4' } }) }} +
+ {{ form_row(form.dateFrom) }} +
+
+ {{ form_row(form.dateTo) }} +
+
+ {{ form_widget(form.apply, { attr: { class: 'btn btn--small' } }) }} + {{ form_widget(form.reset, { attr: { class: 'btn btn--small btn--secondary' } }) }} +
+ {{ form_end(form) }} +
+ + {% if filterDto.active %} +

+ Filter aktiv: + {% if filterDto.dateFrom %}von {{ filterDto.dateFrom|date('d.m.Y') }}{% endif %} + {% if filterDto.dateTo %}bis {{ filterDto.dateTo|date('d.m.Y') }}{% endif %} +

+ {% endif %} + + {% if statistics is empty %} +
+ Keine Daten im ausgewählten Zeitraum vorhanden. +
+ {% else %} +
+ + + + + + + + + + + + {% for stat in statistics %} + + + + + + + + {% endfor %} + + + {% set totalExpected = 0 %} + {% set totalProvided = 0 %} + {% set totalMissing = 0 %} + {% for stat in statistics %} + {% set totalExpected = totalExpected + stat.totalDispositions %} + {% set totalProvided = totalProvided + stat.providedFeedbacks %} + {% set totalMissing = totalMissing + stat.missingFeedbacks %} + {% endfor %} + {% set totalPercentage = totalExpected > 0 ? (totalProvided / totalExpected * 100) : 0 %} + + + + + + + + +
DestinationErwartetAbgegebenFehlendQuote
{{ stat.hotelCode }}{{ stat.totalDispositions }}{{ stat.providedFeedbacks }}{{ stat.missingFeedbacks }} + + {{ stat.percentageProvided|number_format(1, ',', '.') }}% + +
Gesamt{{ totalExpected }}{{ totalProvided }}{{ totalMissing }} + + {{ totalPercentage|number_format(1, ',', '.') }}% + +
+
+ {% endif %} +{% endblock %}