feat: teamer season disposition count in info modal

addresses #869dv94n4
This commit is contained in:
fromme committed 2026-08-31 16:26:53 +02:00
1 parent 39e6f35374
commit 86c5d23181
8 files changed
+344 -2

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Config;
use Carbon\CarbonImmutable;
/**
* The recurring season window, as configured by the "season_start"/"season_end" parameters.
*
* A season is a MM-DD..MM-DD window that normally wraps the turn of the year (01.11.–01.04.)
* and is named after the calendar year it starts in - "Saison 2024/25". It is used to bucket
* a teamer's dispositions per season in the info modal; the date that decides the bucket is
* the assignment's effective start, mirroring App\Repository\Filter\SeasonPeriodFilter.
*
* Both ends are inclusive: an assignment starting on 01.04. still belongs to the season that
* began the previous 01.11. A start date between the two (02.04.–31.10. for the default
* window) belongs to no season and is reported separately.
*/
final class SeasonCalendar
{
public function __construct(
private readonly string $seasonStart,
private readonly string $seasonEnd,
) {
}
/**
* The calendar year the season containing $date starts in, or null when $date lies
* outside any season window.
*/
public function seasonYearFor(\DateTimeInterface $date): ?int
{
$day = CarbonImmutable::instance($date)->startOfDay();
$year = (int) $day->format('Y');
$start = $this->boundaryFor($this->seasonStart, $year);
$end = $this->boundaryFor($this->seasonEnd, $year);
// Wrapping window (the default 01.11.–01.04. case): the season straddles New Year.
if ($end < $start) {
if ($day >= $start) {
return $year;
}
if ($day <= $end) {
return $year - 1;
}
return null;
}
// Non-wrapping window: kept correct should the config ever describe a window that
// stays within one calendar year.
return $day >= $start && $day <= $end ? $year : null;
}
/**
* null -> "außerhalb der Saison"; 2024 -> "Saison 2024/25".
*/
public function label(?int $seasonYear): string
{
if (null === $seasonYear) {
return 'außerhalb der Saison';
}
return sprintf('Saison %d/%02d', $seasonYear, ($seasonYear + 1) % 100);
}
private function boundaryFor(string $monthDay, int $year): CarbonImmutable
{
$boundary = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%d-%s', $year, $monthDay));
if (false === $boundary instanceof CarbonImmutable) {
throw new \InvalidArgumentException(sprintf('Season boundary "%s" is not a valid MM-DD date.', $monthDay));
}
return $boundary->startOfDay();
}
}