From 0c816c11905347b39cecdfe299d01173be726b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 23 Dec 2025 16:24:50 +0100 Subject: [PATCH] feat: average marks per destination addresses #869bgtynb --- ...FeedbackRatingsByDestinationController.php | 49 ++++++++++ src/Menu/AdminMenuBuilder.php | 14 ++- src/Menu/ManagerMenuBuilder.php | 14 ++- src/Repository/FeedbackRepository.php | 90 +++++++++++++++++++ .../feedback_ratings_by_destination.html.twig | 76 ++++++++++++++++ 5 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php create mode 100644 templates/administrative/statistics/feedback_ratings_by_destination.html.twig diff --git a/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php b/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php new file mode 100644 index 0000000..d999214 --- /dev/null +++ b/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php @@ -0,0 +1,49 @@ +filterHandler->getFilterSettings(); + + $form = $this->createForm(StatisticsFilterType::class, $filterDto); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $filterDto = $this->filterHandler->handleRequest($form); + } + + $statistics = $this->feedbackRepository->getAverageRatingsByDestinationAndFeedbackSet( + $filterDto->getDateFrom(), + $filterDto->getDateTo(), + ); + + return $this->render('administrative/statistics/feedback_ratings_by_destination.html.twig', [ + 'form' => $form->createView(), + 'filterDto' => $filterDto, + 'destinations' => $statistics['destinations'], + 'feedbackSets' => $statistics['feedbackSets'], + ]); + } +} diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index 01b9e6f..984ad15 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -123,21 +123,17 @@ class AdminMenuBuilder extends AbstractMenuBuilder 'linkAttributes' => [ 'title' => 'Bewertungen nach Kategorie', ], - 'extras' => [ - 'routes' => [ - ['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/'], - ], + ]); + $statisticsMenu->addChild('Bewertungen nach Destination', [ + 'route' => 'app_administrative_statistics_feedback_ratings_by_destination', + 'linkAttributes' => [ + 'title' => 'Bewertungen nach Destination', ], ]); $settingsMenu = $menu->addChild('Einstellungen', [ diff --git a/src/Menu/ManagerMenuBuilder.php b/src/Menu/ManagerMenuBuilder.php index 99eab18..8a23fb4 100644 --- a/src/Menu/ManagerMenuBuilder.php +++ b/src/Menu/ManagerMenuBuilder.php @@ -104,21 +104,17 @@ class ManagerMenuBuilder extends AbstractMenuBuilder 'linkAttributes' => [ 'title' => 'Bewertungen nach Kategorie', ], - 'extras' => [ - 'routes' => [ - ['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/'], - ], + ]); + $statisticsMenu->addChild('Bewertungen nach Destination', [ + 'route' => 'app_administrative_statistics_feedback_ratings_by_destination', + 'linkAttributes' => [ + 'title' => 'Bewertungen nach Destination', ], ]); $settingsMenu = $menu->addChild('Einstellungen', [ diff --git a/src/Repository/FeedbackRepository.php b/src/Repository/FeedbackRepository.php index 1b89087..e375a1a 100644 --- a/src/Repository/FeedbackRepository.php +++ b/src/Repository/FeedbackRepository.php @@ -118,6 +118,96 @@ class FeedbackRepository extends ServiceEntityRepository ; } + /** + * Returns overall average ratings grouped by normalized destination code and FeedbackSet. + * + * @return array{ + * destinations: array>, + * feedbackSets: array + * } + */ + public function getAverageRatingsByDestinationAndFeedbackSet( + ?\DateTimeImmutable $dateFrom = null, + ?\DateTimeImmutable $dateTo = null, + ): array { + $conn = $this->getEntityManager()->getConnection(); + + $sql = ' + SELECT + CASE + WHEN f.hotel_code LIKE \'SER%\' THEN SUBSTRING(f.hotel_code, 4, 3) + ELSE SUBSTRING(f.hotel_code, 1, 3) + END AS normalized_code, + f.feedback_set_id, + AVG(f.average_rating) AS avg_rating, + COUNT(*) AS feedback_count + FROM feedback f + WHERE f.status = :status + AND f.feedback_set_id IS NOT NULL + AND f.hotel_code IS NOT NULL + '; + + $params = ['status' => Feedback::STATUS_PUBLISHED]; + + if (null !== $dateFrom) { + $sql .= ' AND f.assignment_date_from >= :dateFrom'; + $params['dateFrom'] = $dateFrom->format('Y-m-d'); + } + + if (null !== $dateTo) { + $sql .= ' AND f.assignment_date_to <= :dateTo'; + $params['dateTo'] = $dateTo->format('Y-m-d'); + } + + $sql .= ' + GROUP BY normalized_code, f.feedback_set_id + ORDER BY normalized_code, f.feedback_set_id + '; + + $results = $conn->executeQuery($sql, $params)->fetchAllAssociative(); + + // Collect all feedback set IDs and fetch entities + $feedbackSetIds = array_unique(array_column($results, 'feedback_set_id')); + $feedbackSetRepository = $this->getEntityManager()->getRepository(FeedbackSet::class); + $feedbackSets = []; + + foreach ($feedbackSetIds as $id) { + $feedbackSet = $feedbackSetRepository->find($id); + if (null !== $feedbackSet) { + $feedbackSets[$id] = $feedbackSet; + } + } + + // Build pivot structure: destinations[code][feedbackSetId] = {average, count} + $destinations = []; + + foreach ($results as $row) { + $code = $row['normalized_code']; + $feedbackSetId = (int) $row['feedback_set_id']; + + if (false === isset($feedbackSets[$feedbackSetId])) { + continue; + } + + if (false === isset($destinations[$code])) { + $destinations[$code] = []; + } + + // Convert from percentage (stored as integer * 100) to 1-5 scale + $destinations[$code][$feedbackSetId] = [ + 'average' => round((float) $row['avg_rating'] / 100, 2), + 'count' => (int) $row['feedback_count'], + ]; + } + + ksort($destinations); + + return [ + 'destinations' => $destinations, + 'feedbackSets' => $feedbackSets, + ]; + } + /** * Returns average ratings per question index grouped by FeedbackSet. * diff --git a/templates/administrative/statistics/feedback_ratings_by_destination.html.twig b/templates/administrative/statistics/feedback_ratings_by_destination.html.twig new file mode 100644 index 0000000..b6b7aeb --- /dev/null +++ b/templates/administrative/statistics/feedback_ratings_by_destination.html.twig @@ -0,0 +1,76 @@ +{% extends 'administrative/layout.html.twig' %} + +{% block title %}Bewertungen nach Destination{% endblock %} + +{% block content %} +
+

+ Bewertungen nach 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 destinations is empty %} +
+ Keine Daten im ausgewählten Zeitraum vorhanden. +
+ {% else %} +
+
+ + + + + {% for feedbackSet in feedbackSets %} + + {% endfor %} + + + + {% for code, data in destinations %} + + + {% for feedbackSetId, feedbackSet in feedbackSets %} + + {% endfor %} + + {% endfor %} + +
Destination{{ feedbackSet.name }}
{{ code }} + {% if data[feedbackSetId] is defined %} + {% set avg = data[feedbackSetId].average %} + {% set count = data[feedbackSetId].count %} + + {{ avg|number_format(2, ',', '.') }} + + ({{ count }}) + {% else %} + - + {% endif %} +
+
+
+ {% endif %} +{% endblock %}