feat: average marks per destination

addresses #869bgtynb
This commit is contained in:
Björn Fromme
2025-12-23 16:24:50 +01:00
parent 8b4e7b2f79
commit 0c816c1190
5 changed files with 225 additions and 18 deletions
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Controller\Administrative\Statistics;
use App\Form\StatisticsFilterType;
use App\Repository\FeedbackRepository;
use App\Service\Common\StatisticsFilterHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class FeedbackRatingsByDestinationController extends AbstractController
{
public function __construct(
private readonly StatisticsFilterHandler $filterHandler,
private readonly FeedbackRepository $feedbackRepository,
) {
}
#[Route('/administrative/statistics/feedback-ratings-by-destination', name: 'app_administrative_statistics_feedback_ratings_by_destination')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): Response
{
$filterDto = $this->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'],
]);
}
}
+5 -9
View File
@@ -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', [
+5 -9
View File
@@ -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', [
+90
View File
@@ -118,6 +118,96 @@ class FeedbackRepository extends ServiceEntityRepository
;
}
/**
* Returns overall average ratings grouped by normalized destination code and FeedbackSet.
*
* @return array{
* destinations: array<string, array<int, array{average: float, count: int}>>,
* feedbackSets: array<int, FeedbackSet>
* }
*/
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.
*