From f6667dcfc727c2a2f1b3eca736f3b2bc416dd249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 1 Sep 2026 09:45:30 +0200 Subject: [PATCH] feat: default statistics filter to a rolling 12-month window --- .../ApplicationsByDestinationController.php | 4 +- .../FeedbackByDestinationController.php | 4 +- ...FeedbackRatingsByDestinationController.php | 4 +- .../Statistics/FeedbackRatingsController.php | 4 +- src/Model/StatisticsFilterDto.php | 23 ++++++++ .../applications_by_destination.html.twig | 13 ++-- .../feedback_by_destination.html.twig | 13 ++-- .../statistics/feedback_ratings.html.twig | 13 ++-- .../feedback_ratings_by_destination.html.twig | 13 ++-- tests/Model/StatisticsFilterDtoTest.php | 59 +++++++++++++++++++ 10 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 tests/Model/StatisticsFilterDtoTest.php diff --git a/src/Controller/Administrative/Statistics/ApplicationsByDestinationController.php b/src/Controller/Administrative/Statistics/ApplicationsByDestinationController.php index 7865afd..d3fa1e3 100644 --- a/src/Controller/Administrative/Statistics/ApplicationsByDestinationController.php +++ b/src/Controller/Administrative/Statistics/ApplicationsByDestinationController.php @@ -35,8 +35,8 @@ class ApplicationsByDestinationController extends AbstractController } $statistics = $this->applicationRepository->getApplicationsByDestination( - $filterDto->getDateFrom(), - $filterDto->getDateTo(), + $filterDto->getEffectiveDateFrom(), + $filterDto->getEffectiveDateTo(), ); return $this->render('administrative/statistics/applications_by_destination.html.twig', [ diff --git a/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php b/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php index a839ceb..178a273 100644 --- a/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php +++ b/src/Controller/Administrative/Statistics/FeedbackByDestinationController.php @@ -35,8 +35,8 @@ class FeedbackByDestinationController extends AbstractController } $statistics = $this->dispositionRepository->getFeedbackStatisticsByNormalizedHotelCode( - $filterDto->getDateFrom(), - $filterDto->getDateTo(), + $filterDto->getEffectiveDateFrom(), + $filterDto->getEffectiveDateTo(), ); return $this->render('administrative/statistics/feedback_by_destination.html.twig', [ diff --git a/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php b/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php index d999214..46d367a 100644 --- a/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php +++ b/src/Controller/Administrative/Statistics/FeedbackRatingsByDestinationController.php @@ -35,8 +35,8 @@ class FeedbackRatingsByDestinationController extends AbstractController } $statistics = $this->feedbackRepository->getAverageRatingsByDestinationAndFeedbackSet( - $filterDto->getDateFrom(), - $filterDto->getDateTo(), + $filterDto->getEffectiveDateFrom(), + $filterDto->getEffectiveDateTo(), ); return $this->render('administrative/statistics/feedback_ratings_by_destination.html.twig', [ diff --git a/src/Controller/Administrative/Statistics/FeedbackRatingsController.php b/src/Controller/Administrative/Statistics/FeedbackRatingsController.php index cafb676..6cf9755 100644 --- a/src/Controller/Administrative/Statistics/FeedbackRatingsController.php +++ b/src/Controller/Administrative/Statistics/FeedbackRatingsController.php @@ -35,8 +35,8 @@ class FeedbackRatingsController extends AbstractController } $statistics = $this->feedbackRepository->getAverageRatingsByQuestionAndFeedbackSet( - $filterDto->getDateFrom(), - $filterDto->getDateTo(), + $filterDto->getEffectiveDateFrom(), + $filterDto->getEffectiveDateTo(), ); // Prepare chart data for each feedback set diff --git a/src/Model/StatisticsFilterDto.php b/src/Model/StatisticsFilterDto.php index 3eaead9..eb708b3 100644 --- a/src/Model/StatisticsFilterDto.php +++ b/src/Model/StatisticsFilterDto.php @@ -32,4 +32,27 @@ class StatisticsFilterDto extends AbstractFilterDto return $this; } + + /** + * The lower bound actually applied to a query: the picked date, or a rolling + * 12-month window ending today when nothing is picked. + * + * The default lives here rather than in a nullable property so that + * AbstractFilterDto::isActive() still sees a fresh instance as "not filtered" + * (it compares properties with !==, which no two DateTimeImmutable share). + */ + public function getEffectiveDateFrom(): \DateTimeImmutable + { + return $this->dateFrom ?? (new \DateTimeImmutable('today'))->modify('-12 months'); + } + + public function getEffectiveDateTo(): \DateTimeImmutable + { + return $this->dateTo ?? new \DateTimeImmutable('today 23:59:59'); + } + + public function isUsingDefaultRange(): bool + { + return null === $this->dateFrom && null === $this->dateTo; + } } diff --git a/templates/administrative/statistics/applications_by_destination.html.twig b/templates/administrative/statistics/applications_by_destination.html.twig index 264aaaa..648e2a3 100644 --- a/templates/administrative/statistics/applications_by_destination.html.twig +++ b/templates/administrative/statistics/applications_by_destination.html.twig @@ -24,13 +24,12 @@ {{ 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 %} +

+ Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }} + + {% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %} + +

{% if statistics is empty %}
diff --git a/templates/administrative/statistics/feedback_by_destination.html.twig b/templates/administrative/statistics/feedback_by_destination.html.twig index dc039f7..5ce165c 100644 --- a/templates/administrative/statistics/feedback_by_destination.html.twig +++ b/templates/administrative/statistics/feedback_by_destination.html.twig @@ -24,13 +24,12 @@ {{ 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 %} +

+ Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }} + + {% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %} + +

{% if statistics is empty %}
diff --git a/templates/administrative/statistics/feedback_ratings.html.twig b/templates/administrative/statistics/feedback_ratings.html.twig index 2dc825b..1913b2f 100644 --- a/templates/administrative/statistics/feedback_ratings.html.twig +++ b/templates/administrative/statistics/feedback_ratings.html.twig @@ -24,13 +24,12 @@ {{ 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 %} +

+ Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }} + + {% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %} + +

{% if chartsData is empty %}
diff --git a/templates/administrative/statistics/feedback_ratings_by_destination.html.twig b/templates/administrative/statistics/feedback_ratings_by_destination.html.twig index b6b7aeb..c580257 100644 --- a/templates/administrative/statistics/feedback_ratings_by_destination.html.twig +++ b/templates/administrative/statistics/feedback_ratings_by_destination.html.twig @@ -24,13 +24,12 @@ {{ 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 %} +

+ Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }} + + {% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %} + +

{% if destinations is empty %}
diff --git a/tests/Model/StatisticsFilterDtoTest.php b/tests/Model/StatisticsFilterDtoTest.php new file mode 100644 index 0000000..61717f1 --- /dev/null +++ b/tests/Model/StatisticsFilterDtoTest.php @@ -0,0 +1,59 @@ +modify('-12 months'); + + $this->assertSame( + $expectedFrom->format('Y-m-d'), + $dto->getEffectiveDateFrom()->format('Y-m-d'), + ); + $this->assertSame( + (new \DateTimeImmutable('today'))->format('Y-m-d'), + $dto->getEffectiveDateTo()->format('Y-m-d'), + ); + $this->assertTrue($dto->isUsingDefaultRange()); + $this->assertFalse($dto->isActive()); + } + + public function testAPickedLowerBoundStopsTheFromFallbackButNotTheToFallback(): void + { + $from = new \DateTimeImmutable('2025-03-01'); + + $dto = (new StatisticsFilterDto())->setDateFrom($from); + + $this->assertSame($from, $dto->getEffectiveDateFrom()); + $this->assertSame( + (new \DateTimeImmutable('today'))->format('Y-m-d'), + $dto->getEffectiveDateTo()->format('Y-m-d'), + ); + $this->assertFalse($dto->isUsingDefaultRange()); + $this->assertTrue($dto->isActive()); + } + + public function testBothBoundsPickedAreReturnedVerbatim(): void + { + $from = new \DateTimeImmutable('2025-01-01'); + $to = new \DateTimeImmutable('2025-06-30'); + + $dto = (new StatisticsFilterDto()) + ->setDateFrom($from) + ->setDateTo($to) + ; + + $this->assertSame($from, $dto->getEffectiveDateFrom()); + $this->assertSame($to, $dto->getEffectiveDateTo()); + $this->assertFalse($dto->isUsingDefaultRange()); + } +}