fix: drop status breakdown from applications-by-destination stats

This commit is contained in:
2026-09-01 09:46:01 +02:00
parent f6667dcfc7
commit 0d06bc4271
3 changed files with 13 additions and 36 deletions
+5 -15
View File
@@ -253,12 +253,15 @@ class ApplicationRepository extends ServiceEntityRepository
} }
/** /**
* The raw tally grouped by normalized hotel code, split by application status. * The raw application tally grouped by normalized hotel code.
* *
* Mirrors DispositionRepository::getFeedbackStatisticsByNormalizedHotelCodeQuery() - the * Mirrors DispositionRepository::getFeedbackStatisticsByNormalizedHotelCodeQuery() - the
* "Bewerbungen pro Destination" screen sits next to "Feedback pro Destination" and means 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 * same thing by a destination and a season, so the normalization and the date filter are the
* shared ones. * 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( public function getApplicationsByDestinationQuery(
?\DateTimeImmutable $dateFrom = null, ?\DateTimeImmutable $dateFrom = null,
@@ -272,16 +275,10 @@ class ApplicationRepository extends ServiceEntityRepository
$qb $qb
->select( ->select(
$normalizedHotelCode.' AS hotelCode', $normalizedHotelCode.' AS hotelCode',
'SUM(CASE WHEN application.status = :statusNew THEN 1 ELSE 0 END) AS newCount',
'SUM(CASE WHEN application.status = :statusPending THEN 1 ELSE 0 END) AS pendingCount',
'SUM(CASE WHEN application.status = :statusRejected THEN 1 ELSE 0 END) AS rejectedCount',
'COUNT(application.id) AS totalCount' 'COUNT(application.id) AS totalCount'
) )
->innerJoin('application.assignment', 'assignment') ->innerJoin('application.assignment', 'assignment')
->innerJoin('assignment.destination', 'destination') ->innerJoin('assignment.destination', 'destination')
->setParameter('statusNew', Application::STATUS_NEW)
->setParameter('statusPending', Application::STATUS_PENDING)
->setParameter('statusRejected', Application::STATUS_REJECTED)
->groupBy('hotelCode') ->groupBy('hotelCode')
->orderBy('totalCount', 'DESC') ->orderBy('totalCount', 'DESC')
->addOrderBy('hotelCode', 'ASC') ->addOrderBy('hotelCode', 'ASC')
@@ -293,14 +290,10 @@ class ApplicationRepository extends ServiceEntityRepository
} }
/** /**
* Returns application counts grouped by normalized hotel code (base 3-char code), * Returns the application count grouped by normalized hotel code (base 3-char code).
* split into the three application statuses plus a total.
* *
* @return array<int, array{ * @return array<int, array{
* hotelCode: string, * hotelCode: string,
* new: int,
* pending: int,
* rejected: int,
* total: int * total: int
* }> * }>
*/ */
@@ -310,9 +303,6 @@ class ApplicationRepository extends ServiceEntityRepository
): array { ): array {
return array_map(static fn (array $row): array => [ return array_map(static fn (array $row): array => [
'hotelCode' => $row['hotelCode'], 'hotelCode' => $row['hotelCode'],
'new' => (int) $row['newCount'],
'pending' => (int) $row['pendingCount'],
'rejected' => (int) $row['rejectedCount'],
'total' => (int) $row['totalCount'], 'total' => (int) $row['totalCount'],
], $this->getApplicationsByDestinationQuery($dateFrom, $dateTo)->getResult()); ], $this->getApplicationsByDestinationQuery($dateFrom, $dateTo)->getResult());
} }
@@ -41,39 +41,24 @@
<thead> <thead>
<tr class="bg-gray-50 border-b"> <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-left font-semibold">Destination</th>
<th class="px-4 py-3 text-right font-semibold">Neu</th> <th class="px-4 py-3 text-right font-semibold">Bewerbungen</th>
<th class="px-4 py-3 text-right font-semibold">In Prüfung</th>
<th class="px-4 py-3 text-right font-semibold">Abgelehnt</th>
<th class="px-4 py-3 text-right font-semibold">Gesamt</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for stat in statistics %} {% for stat in statistics %}
<tr class="border-b hover:bg-gray-50"> <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 font-medium">{{ stat.hotelCode }}</td>
<td class="px-4 py-3 text-right">{{ stat.new }}</td> <td class="px-4 py-3 text-right">{{ stat.total }}</td>
<td class="px-4 py-3 text-right">{{ stat.pending }}</td>
<td class="px-4 py-3 text-right">{{ stat.rejected }}</td>
<td class="px-4 py-3 text-right"><span class="font-medium">{{ stat.total }}</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot> <tfoot>
{% set totalNew = 0 %}
{% set totalPending = 0 %}
{% set totalRejected = 0 %}
{% set totalAll = 0 %} {% set totalAll = 0 %}
{% for stat in statistics %} {% for stat in statistics %}
{% set totalNew = totalNew + stat.new %}
{% set totalPending = totalPending + stat.pending %}
{% set totalRejected = totalRejected + stat.rejected %}
{% set totalAll = totalAll + stat.total %} {% set totalAll = totalAll + stat.total %}
{% endfor %} {% endfor %}
<tr class="bg-gray-100 font-semibold"> <tr class="bg-gray-100 font-semibold">
<td class="px-4 py-3">Gesamt</td> <td class="px-4 py-3">Gesamt</td>
<td class="px-4 py-3 text-right">{{ totalNew }}</td>
<td class="px-4 py-3 text-right">{{ totalPending }}</td>
<td class="px-4 py-3 text-right">{{ totalRejected }}</td>
<td class="px-4 py-3 text-right">{{ totalAll }}</td> <td class="px-4 py-3 text-right">{{ totalAll }}</td>
</tr> </tr>
</tfoot> </tfoot>
@@ -86,13 +86,15 @@ class ApplicationRepositoryTest extends KernelTestCase
$this->assertStringContainsString('INNER JOIN assignment.destination destination', $dql); $this->assertStringContainsString('INNER JOIN assignment.destination destination', $dql);
} }
public function testApplicationsByDestinationBindsTheStatusBuckets(): void public function testApplicationsByDestinationCountsEveryApplicationRegardlessOfStatus(): void
{ {
$query = $this->createApplicationsByDestinationQuery(); $query = $this->createApplicationsByDestinationQuery();
$dql = $query->getDQL();
$this->assertSame(Application::STATUS_NEW, $query->getParameter('statusNew')->getValue()); // status moves over an application's lifetime, so it is not broken out - a plain count
$this->assertSame(Application::STATUS_PENDING, $query->getParameter('statusPending')->getValue()); $this->assertStringContainsString('COUNT(application.id)', $dql);
$this->assertSame(Application::STATUS_REJECTED, $query->getParameter('statusRejected')->getValue()); $this->assertStringNotContainsStringIgnoringCase('application.status', $dql);
$this->assertNull($query->getParameter('statusNew'));
} }
public function testApplicationsByDestinationAppliesTheSeasonBoundsOnlyWhenGiven(): void public function testApplicationsByDestinationAppliesTheSeasonBoundsOnlyWhenGiven(): void