Ajout des commentaires dans le code

This commit is contained in:
PTA
2026-08-20 13:22:49 +02:00
parent 4201894e28
commit 5d8c0993a6
154 changed files with 5071 additions and 47 deletions

View File

@@ -9,10 +9,42 @@ use DatePeriod;
use DateTimeImmutable;
use RuntimeException;
/**
* Construction du calendrier annuel d'un agent, du 1er janvier au 31 décembre.
*
* Produit une structure entièrement prête à l'affichage : les 365 jours sont
* calculés côté serveur, avec leurs libellés, classes CSS et infobulles. Le
* JavaScript ne fait que rendre le tableau — c'est ce qui garantit que le
* calendrier reste identique à l'écran, au PDF et à l'impression.
*
* MODÈLE DES QUATRE COLONNES par jour (constante CELL_KEYS) :
* AP = Animation Périscolaire PP = Préparation Périscolaire
* AE = Animation Extrascolaire PE = Préparation Extrascolaire
*
* Le classement d'un créneau dans l'une des quatre cases dépend de son motif et,
* pour les motifs génériques, du caractère scolaire ou non de la journée
* (voir cellForEntry()).
*/
final class AnnualOverviewModel extends BaseModel
{
private const CELL_KEYS = ['AP', 'PP', 'AE', 'PE'];
/**
* Calendrier annuel complet et ses totaux.
*
* Les créneaux sont lus EN UNE FOIS pour toute l'année puis indexés par date
* en mémoire : 365 requêtes seraient inenvisageables.
*
* Trois familles de totaux cumulées en parallèle : par case (AP/PP/AE/PE),
* par statut (validé / brouillon) et par motif particulier (congé,
* formation, absence…). Elles répondent à trois questions différentes et ne
* se déduisent pas les unes des autres.
*
* @return array<string, mixed> agent, year, quota_periods, calendar_totals,
* months (12 entrées) et legend.
*
* @throws RuntimeException Si l'agent est introuvable ou inactif.
*/
public function overview(int $agentId, int $year): array
{
$agentModel = new AgentModel($this->pdo);
@@ -100,6 +132,27 @@ final class AnnualOverviewModel extends BaseModel
];
}
/**
* Construit UNE journée du calendrier à partir de ses créneaux.
*
* Les quatre cases sont toujours présentes, même vides : la grille HTML doit
* garder un nombre de colonnes constant.
*
* Une case peut recevoir plusieurs créneaux ; elle prend alors la classe CSS
* du motif le plus « fort » (voir strongestClass()), et son contenu affiche
* les codes courts plutôt que la durée dès qu'un motif particulier est
* présent — « CA » est plus parlant que « 7h00 » pour un jour de congé.
*
* `structure_id` retient le lieu du PREMIER créneau du jour, ou à défaut le
* rattachement de l'agent : il sert à construire les liens profonds vers
* l'écran de saisie.
*
* @param array<int, array<string, mixed>> $entries Créneaux de la journée.
* @param array<string, mixed>|null $vacation Période de vacances, ou null.
* @param array<string, mixed> $agent Fiche de l'agent.
*
* @return array<string, mixed>
*/
private function buildDay(DateTimeImmutable $date, array $entries, ?array $vacation, array $agent): array
{
$cells = [];
@@ -186,6 +239,18 @@ final class AnnualOverviewModel extends BaseModel
];
}
/**
* Périodes de quota à afficher pour l'année.
*
* Trois dates de contexte sont interrogées (1er janvier, 31 décembre, et le
* début de contrat s'il tombe dans l'année), puis dédoublonnées par leurs
* bornes. Depuis que le quota suit l'année civile, elles donnent toutes la
* même période : le résultat ne contient donc en pratique qu'une entrée. La
* mécanique est conservée parce qu'elle reste juste si une règle de
* proratisation en cours d'année est réintroduite.
*
* @return array<int, array<string, mixed>>
*/
private function quotaPeriodsForYear(int $agentId, int $year, array $agent): array
{
$quotaModel = new QuotaModel($this->pdo);
@@ -210,6 +275,14 @@ final class AnnualOverviewModel extends BaseModel
return array_values($periods);
}
/**
* Choisit la case (AP/PP/AE/PE) d'un créneau selon son motif.
*
* Un motif non reconnu bascule sur le contexte de la journée : animation
* extrascolaire pendant les vacances, périscolaire sinon. C'est le
* comportement le moins surprenant pour un motif ajouté en base après coup,
* et il évite qu'un temps disparaisse du calendrier.
*/
private function cellForEntry(string $code, bool $isVacation): string
{
return match ($code) {
@@ -222,6 +295,12 @@ final class AnnualOverviewModel extends BaseModel
};
}
/**
* Abréviation affichée dans la case pour les motifs PARTICULIERS.
*
* @return string|null null pour les motifs de travail ordinaires, qui
* affichent alors leur durée plutôt qu'un code.
*/
private function shortCode(string $code): ?string
{
return match ($code) {
@@ -236,6 +315,10 @@ final class AnnualOverviewModel extends BaseModel
};
}
/**
* Classe CSS d'un créneau, à partir de son motif et, à défaut, de sa case.
* Les classes correspondantes sont définies dans assets/style.css.
*/
private function classForCode(string $code, string $cellKey): string
{
return match ($code) {
@@ -251,6 +334,17 @@ final class AnnualOverviewModel extends BaseModel
};
}
/**
* Arbitre la classe d'une case contenant plusieurs motifs.
*
* Une seule couleur par case : c'est la plus « forte » qui l'emporte, selon
* une priorité croissante allant du travail ordinaire à la journée non
* travaillée. Une demi-journée de congé doit rester visible à côté d'une
* demi-journée travaillée ; l'inverse ferait disparaître l'information.
*
* Le `>=` fait gagner le dernier créneau à priorité égale, ce qui est sans
* conséquence puisque la classe est alors la même.
*/
private function strongestClass(string $current, string $candidate): string
{
$priority = [
@@ -268,6 +362,12 @@ final class AnnualOverviewModel extends BaseModel
return ($priority[$candidate] ?? 0) >= ($priority[$current] ?? 0) ? $candidate : $current;
}
/**
* Contenu textuel d'une case : rien si vide, les codes courts s'il y en a,
* la durée compacte sinon.
*
* @param array<int, string> $codes
*/
private function cellDisplay(int $minutes, array $codes): string
{
if ($minutes <= 0) {
@@ -279,6 +379,15 @@ final class AnnualOverviewModel extends BaseModel
return $this->formatCompactMinutes($minutes);
}
/**
* Période de vacances contenant cette date, ou null.
* Copie locale de VacationModel::vacationForDate() : les périodes sont déjà
* chargées en mémoire pour toute l'année, inutile de repasser par le modèle.
*
* @param array<int, array<string, mixed>> $vacations
*
* @return array<string, mixed>|null
*/
private function vacationForDate(string $date, array $vacations): ?array
{
foreach ($vacations as $vacation) {
@@ -289,6 +398,7 @@ final class AnnualOverviewModel extends BaseModel
return null;
}
/** Durée en minutes entre deux HH:MM, plancher à 0. */
private function minutesBetween(string $start, string $end): int
{
[$startHour, $startMinute] = array_map('intval', explode(':', $start));
@@ -296,6 +406,14 @@ final class AnnualOverviewModel extends BaseModel
return max(0, ($endHour * 60 + $endMinute) - ($startHour * 60 + $startMinute));
}
/**
* Décline chaque total en trois formes : minutes brutes, durée lisible
* (« 7 h 30 ») et forme compacte (« 7h30 ») pour les cases étroites.
*
* @param array<string, int> $totals
*
* @return array<string, array{minutes: int, duration: string, compact: string}>
*/
private function formatTotals(array $totals): array
{
$formatted = [];
@@ -309,6 +427,7 @@ final class AnnualOverviewModel extends BaseModel
return $formatted;
}
/** 450 → « 7 h 30 ». Gère les valeurs négatives (signe extrait d'abord). */
private function formatMinutes(int $minutes): string
{
$sign = $minutes < 0 ? '-' : '';
@@ -316,6 +435,7 @@ final class AnnualOverviewModel extends BaseModel
return sprintf('%s%d h %02d', $sign, intdiv($minutes, 60), $minutes % 60);
}
/** 450 → « 7h30 », 420 → « 7h », 0 → chaîne vide. Pour les cases étroites. */
private function formatCompactMinutes(int $minutes): string
{
if ($minutes <= 0) {
@@ -326,6 +446,7 @@ final class AnnualOverviewModel extends BaseModel
return $mins === 0 ? $hours . 'h' : sprintf('%dh%02d', $hours, $mins);
}
/** Nom du mois en français. Table en dur : le conteneur n'a pas de locale fr. */
private function monthName(int $month): string
{
return [
@@ -335,6 +456,7 @@ final class AnnualOverviewModel extends BaseModel
][$month];
}
/** Initiale du jour (1 = lundi). Mardi et Mercredi partagent « M ». */
private function weekdayShort(int $weekday): string
{
return [1 => 'L', 2 => 'M', 3 => 'M', 4 => 'J', 5 => 'V', 6 => 'S', 7 => 'D'][$weekday];