Compare commits
6
Commits
84441d9e69
...
2f747fc22f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f747fc22f | ||
|
|
0d06bc4271 | ||
|
|
f6667dcfc7 | ||
|
|
410c99a9b0 | ||
|
|
b2cc6e2dcf | ||
|
|
e66351a2cc |
No files matched your search
Generated
+6
-6
@@ -12325,16 +12325,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.95.23",
|
||||
"version": "v3.95.24",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4"
|
||||
"reference": "9a4b8050f8768646bf3693ee7416978ceeca6206"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b2932a5af3157a0c28324b1efe5e3b556dee09c4",
|
||||
"reference": "b2932a5af3157a0c28324b1efe5e3b556dee09c4",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9a4b8050f8768646bf3693ee7416978ceeca6206",
|
||||
"reference": "9a4b8050f8768646bf3693ee7416978ceeca6206",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12417,7 +12417,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.23"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.24"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -12425,7 +12425,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-26T19:54:54+00:00"
|
||||
"time": "2026-08-31T15:31:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "marcocesarato/php-conventional-changelog",
|
||||
|
||||
-18
@@ -69,24 +69,6 @@ $rsyncOptions = [
|
||||
'timeout' => 300,
|
||||
];
|
||||
|
||||
// The provider's rsync (3.5.0) fails to chdir into a destination whose path traverses
|
||||
// symlinks, which /home/www/p704161 does: `change_dir#1 ... Too many levels of symbolic
|
||||
// links (40)`. Verified: the same directory rsyncs fine when addressed relative to $HOME.
|
||||
// Only contrib/rsync.php hands a path to rsync; every other task goes through the SSH
|
||||
// shell, which resolves the absolute path without trouble, so deploy_path stays absolute.
|
||||
set('remote_home', '/home/www/p704161');
|
||||
|
||||
set('rsync_dest', function () {
|
||||
$releasePath = get('release_path');
|
||||
$home = rtrim(get('remote_home'), '/').'/';
|
||||
|
||||
if (!str_starts_with($releasePath, $home)) {
|
||||
return $releasePath;
|
||||
}
|
||||
|
||||
return substr($releasePath, strlen($home));
|
||||
});
|
||||
|
||||
host('prod')
|
||||
->setHostname('185.243.135.29')
|
||||
->setRemoteUser('p704161')
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Administrative\Statistics;
|
||||
|
||||
use App\Form\StatisticsFilterType;
|
||||
use App\Repository\ApplicationRepository;
|
||||
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 ApplicationsByDestinationController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StatisticsFilterHandler $filterHandler,
|
||||
private readonly ApplicationRepository $applicationRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/statistics/applications-by-destination', name: 'app_administrative_statistics_applications_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()) {
|
||||
$this->filterHandler->handleRequest($form);
|
||||
|
||||
return $this->redirectToRoute('app_administrative_statistics_applications_by_destination');
|
||||
}
|
||||
|
||||
$statistics = $this->applicationRepository->getApplicationsByDestination(
|
||||
$filterDto->getEffectiveDateFrom(),
|
||||
$filterDto->getEffectiveDateTo(),
|
||||
);
|
||||
|
||||
return $this->render('administrative/statistics/applications_by_destination.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'filterDto' => $filterDto,
|
||||
'statistics' => $statistics,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,14 @@ class FeedbackByDestinationController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$filterDto = $this->filterHandler->handleRequest($form);
|
||||
$this->filterHandler->handleRequest($form);
|
||||
|
||||
return $this->redirectToRoute('app_administrative_statistics_feedback_by_destination');
|
||||
}
|
||||
|
||||
$statistics = $this->dispositionRepository->getFeedbackStatisticsByNormalizedHotelCode(
|
||||
$filterDto->getDateFrom(),
|
||||
$filterDto->getDateTo(),
|
||||
$filterDto->getEffectiveDateFrom(),
|
||||
$filterDto->getEffectiveDateTo(),
|
||||
);
|
||||
|
||||
return $this->render('administrative/statistics/feedback_by_destination.html.twig', [
|
||||
|
||||
@@ -31,12 +31,14 @@ class FeedbackRatingsByDestinationController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$filterDto = $this->filterHandler->handleRequest($form);
|
||||
$this->filterHandler->handleRequest($form);
|
||||
|
||||
return $this->redirectToRoute('app_administrative_statistics_feedback_ratings_by_destination');
|
||||
}
|
||||
|
||||
$statistics = $this->feedbackRepository->getAverageRatingsByDestinationAndFeedbackSet(
|
||||
$filterDto->getDateFrom(),
|
||||
$filterDto->getDateTo(),
|
||||
$filterDto->getEffectiveDateFrom(),
|
||||
$filterDto->getEffectiveDateTo(),
|
||||
);
|
||||
|
||||
return $this->render('administrative/statistics/feedback_ratings_by_destination.html.twig', [
|
||||
|
||||
@@ -31,12 +31,14 @@ class FeedbackRatingsController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$filterDto = $this->filterHandler->handleRequest($form);
|
||||
$this->filterHandler->handleRequest($form);
|
||||
|
||||
return $this->redirectToRoute('app_administrative_statistics_feedback_ratings');
|
||||
}
|
||||
|
||||
$statistics = $this->feedbackRepository->getAverageRatingsByQuestionAndFeedbackSet(
|
||||
$filterDto->getDateFrom(),
|
||||
$filterDto->getDateTo(),
|
||||
$filterDto->getEffectiveDateFrom(),
|
||||
$filterDto->getEffectiveDateTo(),
|
||||
);
|
||||
|
||||
// Prepare chart data for each feedback set
|
||||
|
||||
@@ -136,6 +136,12 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
'title' => 'Bewertungen nach Destination',
|
||||
],
|
||||
]);
|
||||
$statisticsMenu->addChild('Bewerbungen pro Destination', [
|
||||
'route' => 'app_administrative_statistics_applications_by_destination',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Bewerbungen pro Destination',
|
||||
],
|
||||
]);
|
||||
$settingsMenu = $menu->addChild('Einstellungen', [
|
||||
'linkAttributes' => [
|
||||
'title' => 'Einstellungen',
|
||||
|
||||
@@ -117,6 +117,12 @@ class ManagerMenuBuilder extends AbstractMenuBuilder
|
||||
'title' => 'Bewertungen nach Destination',
|
||||
],
|
||||
]);
|
||||
$statisticsMenu->addChild('Bewerbungen pro Destination', [
|
||||
'route' => 'app_administrative_statistics_applications_by_destination',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Bewerbungen pro Destination',
|
||||
],
|
||||
]);
|
||||
$settingsMenu = $menu->addChild('Einstellungen', [
|
||||
'linkAttributes' => [
|
||||
'title' => 'Einstellungen',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Entity\Assignment;
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\ApplicationFilterDto;
|
||||
use App\Model\AssignmentFilterDto;
|
||||
use App\Repository\Filter\SeasonPeriodFilter;
|
||||
use App\Repository\Traits\QueryHelperTrait;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -250,4 +251,59 @@ class ApplicationRepository extends ServiceEntityRepository
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw application tally grouped by normalized hotel code.
|
||||
*
|
||||
* Mirrors DispositionRepository::getFeedbackStatisticsByNormalizedHotelCodeQuery() - the
|
||||
* "Bewerbungen pro Destination" screen sits next to "Feedback pro Destination" and means the
|
||||
* same thing by a destination and a season, so the normalization and the date filter are the
|
||||
* shared ones.
|
||||
*
|
||||
* Status is deliberately not broken out: an application's status moves over its lifetime, so a
|
||||
* snapshot of the current status is not a meaningful figure for a past period.
|
||||
*/
|
||||
public function getApplicationsByDestinationQuery(
|
||||
?\DateTimeImmutable $dateFrom = null,
|
||||
?\DateTimeImmutable $dateTo = null,
|
||||
): Query {
|
||||
$qb = $this->createQueryBuilder('application');
|
||||
|
||||
// Normalize hotel code: strip SER prefix to get base 3-char code
|
||||
$normalizedHotelCode = "CASE WHEN destination.hotelCode LIKE 'SER%' THEN SUBSTRING(destination.hotelCode, 4, 3) ELSE SUBSTRING(destination.hotelCode, 1, 3) END";
|
||||
|
||||
$qb
|
||||
->select(
|
||||
$normalizedHotelCode.' AS hotelCode',
|
||||
'COUNT(application.id) AS totalCount'
|
||||
)
|
||||
->innerJoin('application.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->groupBy('hotelCode')
|
||||
->orderBy('totalCount', 'DESC')
|
||||
->addOrderBy('hotelCode', 'ASC')
|
||||
;
|
||||
|
||||
SeasonPeriodFilter::apply($qb, $dateFrom, $dateTo);
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application count grouped by normalized hotel code (base 3-char code).
|
||||
*
|
||||
* @return array<int, array{
|
||||
* hotelCode: string,
|
||||
* total: int
|
||||
* }>
|
||||
*/
|
||||
public function getApplicationsByDestination(
|
||||
?\DateTimeImmutable $dateFrom = null,
|
||||
?\DateTimeImmutable $dateTo = null,
|
||||
): array {
|
||||
return array_map(static fn (array $row): array => [
|
||||
'hotelCode' => $row['hotelCode'],
|
||||
'total' => (int) $row['totalCount'],
|
||||
], $this->getApplicationsByDestinationQuery($dateFrom, $dateTo)->getResult());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends 'administrative/layout.html.twig' %}
|
||||
|
||||
{% block title %}Bewerbungen pro Destination{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex items-start justify-between pb-4">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Bewerbungen pro Destination
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-8">
|
||||
{{ form_start(form, { attr: { class: 'flex flex-wrap items-end gap-4' } }) }}
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
{{ form_row(form.dateFrom) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
{{ form_row(form.dateTo) }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{{ form_widget(form.apply, { attr: { class: 'btn btn--small' } }) }}
|
||||
{{ form_widget(form.reset, { attr: { class: 'btn btn--small btn--secondary' } }) }}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }}
|
||||
<span class="text-gray-400">
|
||||
{% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{% if statistics is empty %}
|
||||
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||||
Keine Daten im ausgewählten Zeitraum vorhanden.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 border-b">
|
||||
<th class="px-4 py-3 text-left font-semibold">Destination</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">Bewerbungen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in statistics %}
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-medium">{{ stat.hotelCode }}</td>
|
||||
<td class="px-4 py-3 text-right">{{ stat.total }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{% set totalAll = 0 %}
|
||||
{% for stat in statistics %}
|
||||
{% set totalAll = totalAll + stat.total %}
|
||||
{% endfor %}
|
||||
<tr class="bg-gray-100 font-semibold">
|
||||
<td class="px-4 py-3">Gesamt</td>
|
||||
<td class="px-4 py-3 text-right">{{ totalAll }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -24,13 +24,12 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
{% if filterDto.active %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Filter aktiv:
|
||||
{% if filterDto.dateFrom %}von {{ filterDto.dateFrom|date('d.m.Y') }}{% endif %}
|
||||
{% if filterDto.dateTo %}bis {{ filterDto.dateTo|date('d.m.Y') }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }}
|
||||
<span class="text-gray-400">
|
||||
{% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{% if statistics is empty %}
|
||||
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||||
@@ -57,9 +56,9 @@
|
||||
<td class="px-4 py-3 text-right">{{ stat.missingFeedbacks }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<span class="{{ html_classes('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
|
||||
'bg-green-100 text-green-800': totalPercentage >= 80,
|
||||
'bg-yellow-100 text-yellow-800': totalPercentage >= 50,
|
||||
'bg-red-100 text-red-800': totalPercentage >= 0
|
||||
'bg-green-100 text-green-800': stat.percentageProvided >= 80,
|
||||
'bg-yellow-100 text-yellow-800': stat.percentageProvided >= 50 and stat.percentageProvided < 80,
|
||||
'bg-red-100 text-red-800': stat.percentageProvided < 50
|
||||
}) }}">{{ stat.percentageProvided|number_format(1, ',', '.') }}%</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -83,8 +82,8 @@
|
||||
<td class="px-4 py-3 text-right">
|
||||
<span class="{{ html_classes('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
|
||||
'bg-green-100 text-green-800': totalPercentage >= 80,
|
||||
'bg-yellow-100 text-yellow-800': totalPercentage >= 50,
|
||||
'bg-red-100 text-red-800': totalPercentage >= 0
|
||||
'bg-yellow-100 text-yellow-800': totalPercentage >= 50 and totalPercentage < 80,
|
||||
'bg-red-100 text-red-800': totalPercentage < 50
|
||||
}) }}">{{ totalPercentage|number_format(1, ',', '.') }}%</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
{% if filterDto.active %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Filter aktiv:
|
||||
{% if filterDto.dateFrom %}von {{ filterDto.dateFrom|date('d.m.Y') }}{% endif %}
|
||||
{% if filterDto.dateTo %}bis {{ filterDto.dateTo|date('d.m.Y') }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }}
|
||||
<span class="text-gray-400">
|
||||
{% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{% if chartsData is empty %}
|
||||
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
{% if filterDto.active %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Filter aktiv:
|
||||
{% if filterDto.dateFrom %}von {{ filterDto.dateFrom|date('d.m.Y') }}{% endif %}
|
||||
{% if filterDto.dateTo %}bis {{ filterDto.dateTo|date('d.m.Y') }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Zeitraum: {{ filterDto.effectiveDateFrom|date('d.m.Y') }} – {{ filterDto.effectiveDateTo|date('d.m.Y') }}
|
||||
<span class="text-gray-400">
|
||||
{% if filterDto.usingDefaultRange %}(Standard: letzte 12 Monate){% else %}(Filter aktiv){% endif %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{% if destinations is empty %}
|
||||
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Model;
|
||||
|
||||
use App\Model\StatisticsFilterDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StatisticsFilterDtoTest extends TestCase
|
||||
{
|
||||
public function testFreshDtoFallsBackToARollingTwelveMonthWindow(): void
|
||||
{
|
||||
$dto = new StatisticsFilterDto();
|
||||
|
||||
$expectedFrom = (new \DateTimeImmutable('today'))->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());
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,61 @@ class ApplicationRepositoryTest extends KernelTestCase
|
||||
$period ?? new CarbonPeriodImmutable('2025-01-10', '2025-01-20')
|
||||
);
|
||||
}
|
||||
|
||||
public function testApplicationsByDestinationGroupsByTheNormalizedHotelCode(): void
|
||||
{
|
||||
$dql = $this->createApplicationsByDestinationQuery()->getDQL();
|
||||
|
||||
// the SER prefix is stripped so a house counts once, however its code is spelled
|
||||
$this->assertStringContainsString('SUBSTRING(destination.hotelCode, 4, 3)', $dql);
|
||||
$this->assertStringContainsString('SUBSTRING(destination.hotelCode, 1, 3)', $dql);
|
||||
$this->assertStringContainsString('GROUP BY hotelCode', $dql);
|
||||
|
||||
// a destination lives on the assignment, not the application
|
||||
$this->assertStringContainsString('INNER JOIN application.assignment assignment', $dql);
|
||||
$this->assertStringContainsString('INNER JOIN assignment.destination destination', $dql);
|
||||
}
|
||||
|
||||
public function testApplicationsByDestinationCountsEveryApplicationRegardlessOfStatus(): void
|
||||
{
|
||||
$query = $this->createApplicationsByDestinationQuery();
|
||||
$dql = $query->getDQL();
|
||||
|
||||
// status moves over an application's lifetime, so it is not broken out - a plain count
|
||||
$this->assertStringContainsString('COUNT(application.id)', $dql);
|
||||
$this->assertStringNotContainsStringIgnoringCase('application.status', $dql);
|
||||
$this->assertNull($query->getParameter('statusNew'));
|
||||
}
|
||||
|
||||
public function testApplicationsByDestinationAppliesTheSeasonBoundsOnlyWhenGiven(): void
|
||||
{
|
||||
$unfiltered = $this->createApplicationsByDestinationQuery();
|
||||
$this->assertNull($unfiltered->getParameter('dateFrom'));
|
||||
$this->assertNull($unfiltered->getParameter('dateTo'));
|
||||
|
||||
$filtered = $this->createApplicationsByDestinationQuery(
|
||||
new \DateTimeImmutable('2025-05-01'),
|
||||
new \DateTimeImmutable('2025-09-30'),
|
||||
);
|
||||
$dql = $filtered->getDQL();
|
||||
|
||||
// the season is the destination's date range, same as every other statistics screen
|
||||
$this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql);
|
||||
$this->assertStringContainsString('destination.dateTo <= :dateTo', $dql);
|
||||
$this->assertSame('2025-05-01', $filtered->getParameter('dateFrom')->getValue()->format('Y-m-d'));
|
||||
$this->assertSame('2025-09-30', $filtered->getParameter('dateTo')->getValue()->format('Y-m-d'));
|
||||
}
|
||||
|
||||
private function createApplicationsByDestinationQuery(
|
||||
?\DateTimeImmutable $dateFrom = null,
|
||||
?\DateTimeImmutable $dateTo = null,
|
||||
): Query {
|
||||
/** @var EntityManagerInterface $entityManager */
|
||||
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
/** @var ApplicationRepository $repository */
|
||||
$repository = $entityManager->getRepository(Application::class);
|
||||
|
||||
return $repository->getApplicationsByDestinationQuery($dateFrom, $dateTo);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user