feat: applications by destination statistics

addresses #869dv9fh6
This commit is contained in:
2026-09-01 09:28:41 +02:00
parent e66351a2cc
commit b2cc6e2dcf
6 changed files with 265 additions and 0 deletions
@@ -0,0 +1,48 @@
<?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()) {
$filterDto = $this->filterHandler->handleRequest($form);
}
$statistics = $this->applicationRepository->getApplicationsByDestination(
$filterDto->getDateFrom(),
$filterDto->getDateTo(),
);
return $this->render('administrative/statistics/applications_by_destination.html.twig', [
'form' => $form->createView(),
'filterDto' => $filterDto,
'statistics' => $statistics,
]);
}
}
+6
View File
@@ -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',
+6
View File
@@ -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',
+66
View File
@@ -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,69 @@ class ApplicationRepository extends ServiceEntityRepository
->getSingleScalarResult()
;
}
/**
* The raw tally grouped by normalized hotel code, split by application status.
*
* 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.
*/
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',
'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'
)
->innerJoin('application.assignment', 'assignment')
->innerJoin('assignment.destination', 'destination')
->setParameter('statusNew', Application::STATUS_NEW)
->setParameter('statusPending', Application::STATUS_PENDING)
->setParameter('statusRejected', Application::STATUS_REJECTED)
->groupBy('hotelCode')
->orderBy('totalCount', 'DESC')
->addOrderBy('hotelCode', 'ASC')
;
SeasonPeriodFilter::apply($qb, $dateFrom, $dateTo);
return $qb->getQuery();
}
/**
* Returns application counts grouped by normalized hotel code (base 3-char code),
* split into the three application statuses plus a total.
*
* @return array<int, array{
* hotelCode: string,
* new: int,
* pending: int,
* rejected: int,
* total: int
* }>
*/
public function getApplicationsByDestination(
?\DateTimeImmutable $dateFrom = null,
?\DateTimeImmutable $dateTo = null,
): array {
return array_map(static fn (array $row): array => [
'hotelCode' => $row['hotelCode'],
'new' => (int) $row['newCount'],
'pending' => (int) $row['pendingCount'],
'rejected' => (int) $row['rejectedCount'],
'total' => (int) $row['totalCount'],
], $this->getApplicationsByDestinationQuery($dateFrom, $dateTo)->getResult());
}
}