pour prod

This commit is contained in:
Loic Masi
2026-08-07 16:13:43 +02:00
commit 5fbf76868f
157 changed files with 24085 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
namespace App\Models;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
use RuntimeException;
final class AnnualOverviewModel extends BaseModel
{
private const CELL_KEYS = ['AP', 'PP', 'AE', 'PE'];
public function overview(int $agentId, int $year): array
{
$agentModel = new AgentModel($this->pdo);
$agent = $agentModel->findActive($agentId);
if ($agent === null) {
throw new RuntimeException('Agent introuvable.');
}
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
$entries = $agentModel->entriesForDateRange($agentId, $start, $end);
$vacations = (new VacationModel($this->pdo))->periodsBetween($start, $end);
$quotaPeriods = $this->quotaPeriodsForYear($agentId, $year, $agent);
$entriesByDate = [];
foreach ($entries as $entry) {
$entriesByDate[(string) $entry['date_jour']][] = $entry;
}
$months = [];
$yearTotals = array_fill_keys(self::CELL_KEYS, 0);
$statusTotals = ['VALIDE' => 0, 'BROUILLON' => 0];
$specialTotals = [];
for ($month = 1; $month <= 12; $month++) {
$monthStart = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
$monthEnd = $monthStart->modify('last day of this month');
$days = [];
$monthTotals = array_fill_keys(self::CELL_KEYS, 0);
$period = new DatePeriod($monthStart, new DateInterval('P1D'), $monthEnd->modify('+1 day'));
foreach ($period as $date) {
$dateValue = $date->format('Y-m-d');
$vacation = $this->vacationForDate($dateValue, $vacations);
$day = $this->buildDay($date, $entriesByDate[$dateValue] ?? [], $vacation, $agent);
$days[] = $day;
foreach (self::CELL_KEYS as $key) {
$minutes = (int) $day['cells'][$key]['minutes'];
$monthTotals[$key] += $minutes;
$yearTotals[$key] += $minutes;
}
foreach ($day['status_minutes'] as $status => $minutes) {
$statusTotals[$status] = ($statusTotals[$status] ?? 0) + $minutes;
}
foreach ($day['special_minutes'] as $code => $minutes) {
$specialTotals[$code] = ($specialTotals[$code] ?? 0) + $minutes;
}
}
$months[] = [
'number' => $month,
'name' => $this->monthName($month),
'days' => $days,
'totals' => $this->formatTotals($monthTotals),
];
}
$calendarMinutes = array_sum($yearTotals);
return [
'agent' => $agent,
'year' => $year,
'generated_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
'quota_periods' => $quotaPeriods,
'calendar_totals' => [
'minutes' => $calendarMinutes,
'duration' => $this->formatMinutes($calendarMinutes),
'cells' => $this->formatTotals($yearTotals),
'validated' => $this->formatMinutes((int) ($statusTotals['VALIDE'] ?? 0)),
'draft' => $this->formatMinutes((int) ($statusTotals['BROUILLON'] ?? 0)),
'specials' => array_map(fn(int $minutes): string => $this->formatMinutes($minutes), $specialTotals),
],
'months' => $months,
'legend' => [
['code' => 'TRAVAIL', 'label' => 'Temps de travail', 'class' => 'annual-work'],
['code' => 'PREPA', 'label' => 'Temps de préparation', 'class' => 'annual-preparation'],
['code' => 'FOR', 'label' => 'Formation', 'class' => 'annual-training'],
['code' => 'CA', 'label' => 'Congé annuel', 'class' => 'annual-leave'],
['code' => 'ABS', 'label' => 'Absence', 'class' => 'annual-absence'],
['code' => 'JNT', 'label' => 'Journée non travaillée', 'class' => 'annual-jnt'],
['code' => 'JF', 'label' => 'Journée de fractionnement', 'class' => 'annual-fraction'],
['code' => 'VAC', 'label' => 'Vacances scolaires', 'class' => 'annual-vacation'],
],
];
}
private function buildDay(DateTimeImmutable $date, array $entries, ?array $vacation, array $agent): array
{
$cells = [];
foreach (self::CELL_KEYS as $key) {
$cells[$key] = [
'minutes' => 0,
'display' => '',
'details' => [],
'codes' => [],
'class' => '',
'structure_id' => null,
];
}
$statusMinutes = ['VALIDE' => 0, 'BROUILLON' => 0];
$specialMinutes = [];
$hasDraft = false;
$firstStructureId = null;
foreach ($entries as $entry) {
$minutes = $this->minutesBetween((string) $entry['heure_debut'], (string) $entry['heure_fin']);
if ($minutes <= 0) {
continue;
}
$code = strtoupper((string) $entry['motif_code']);
$cellKey = $this->cellForEntry($code, $vacation !== null);
$shortCode = $this->shortCode($code);
$status = strtoupper((string) $entry['statut']) === 'VALIDE' ? 'VALIDE' : 'BROUILLON';
$statusMinutes[$status] += $minutes;
$hasDraft = $hasDraft || $status === 'BROUILLON';
$firstStructureId ??= isset($entry['id_structure']) ? (int) $entry['id_structure'] : null;
$cells[$cellKey]['minutes'] += $minutes;
$cells[$cellKey]['structure_id'] ??= isset($entry['id_structure']) ? (int) $entry['id_structure'] : null;
$cells[$cellKey]['details'][] = sprintf(
'%s%s · %s · %s%s',
(string) $entry['heure_debut'],
(string) $entry['heure_fin'],
(string) $entry['motif_libelle'],
(string) $entry['structure_nom'],
$status === 'BROUILLON' ? ' · Brouillon' : ''
);
if ($shortCode !== null) {
$cells[$cellKey]['codes'][$shortCode] = true;
$specialMinutes[$code] = ($specialMinutes[$code] ?? 0) + $minutes;
}
$cells[$cellKey]['class'] = $this->strongestClass(
(string) $cells[$cellKey]['class'],
$this->classForCode($code, $cellKey)
);
}
foreach (self::CELL_KEYS as $key) {
$codes = array_keys($cells[$key]['codes']);
$cells[$key]['display'] = $this->cellDisplay((int) $cells[$key]['minutes'], $codes);
$cells[$key]['title'] = implode("\n", $cells[$key]['details']);
unset($cells[$key]['details'], $cells[$key]['codes']);
}
$weekday = (int) $date->format('N');
$week = (int) $date->format('W');
$principalStructure = isset($agent['id_structure']) && $agent['id_structure'] !== null
? (int) $agent['id_structure']
: null;
return [
'date' => $date->format('Y-m-d'),
'day' => (int) $date->format('j'),
'weekday' => $weekday,
'weekday_short' => $this->weekdayShort($weekday),
'week' => $week,
'week_year' => (int) $date->format('o'),
'show_week' => $weekday === 1 || (int) $date->format('j') === 1,
'is_weekend' => $weekday >= 6,
'is_vacation' => $vacation !== null,
'vacation_label' => $vacation['libelle'] ?? null,
'has_entries' => $entries !== [],
'has_draft' => $hasDraft,
'structure_id' => $firstStructureId ?? $principalStructure,
'cells' => $cells,
'status_minutes' => $statusMinutes,
'special_minutes' => $specialMinutes,
];
}
private function quotaPeriodsForYear(int $agentId, int $year, array $agent): array
{
$quotaModel = new QuotaModel($this->pdo);
$contexts = [sprintf('%04d-01-01', $year), sprintf('%04d-12-31', $year)];
$contractStart = trim((string) ($agent['date_debut_contrat'] ?? ''));
if ($contractStart !== '' && str_starts_with($contractStart, (string) $year)) {
$contexts[] = $contractStart;
}
$periods = [];
foreach ($contexts as $context) {
$quota = $quotaModel->forDateForApi($agentId, $context);
$key = $quota['date_debut_periode'] . '|' . $quota['date_fin_periode'];
$periods[$key] = $quota;
}
uasort($periods, static fn(array $a, array $b): int => strcmp(
(string) $a['date_debut_periode'],
(string) $b['date_debut_periode']
));
return array_values($periods);
}
private function cellForEntry(string $code, bool $isVacation): string
{
return match ($code) {
'PREPARATION_PERISCOLAIRE', 'PREPARATION_LUNDI' => 'PP',
'PREPARATION_EXTRASCOLAIRE' => 'PE',
'EXTRASCOLAIRE' => 'AE',
'ALP_MATIN', 'RESTAURATION', 'ALP_SOIR', 'MERCREDI_PERISCOLAIRE' => 'AP',
'JOUR_FRACTIONNEMENT' => 'AP',
default => $isVacation ? 'AE' : 'AP',
};
}
private function shortCode(string $code): ?string
{
return match ($code) {
'FORMATION' => 'FOR',
'CONGE' => 'CA',
'ABSENCE', 'MALADIE' => 'ABS',
'AUTRE_ABSENCE' => 'A.ND',
'JNT' => 'JNT',
'JOUR_FRACTIONNEMENT' => 'JF',
'MENAGE_FOND' => 'MF',
default => null,
};
}
private function classForCode(string $code, string $cellKey): string
{
return match ($code) {
'FORMATION' => 'annual-training',
'CONGE' => 'annual-leave',
'ABSENCE', 'MALADIE' => 'annual-absence',
'AUTRE_ABSENCE' => 'annual-other-absence',
'JNT' => 'annual-jnt',
'JOUR_FRACTIONNEMENT' => 'annual-fraction',
'PREPARATION_PERISCOLAIRE', 'PREPARATION_EXTRASCOLAIRE', 'PREPARATION_LUNDI' => 'annual-preparation',
'EXTRASCOLAIRE' => 'annual-extra',
default => in_array($cellKey, ['PP', 'PE'], true) ? 'annual-preparation' : 'annual-work',
};
}
private function strongestClass(string $current, string $candidate): string
{
$priority = [
'' => 0,
'annual-work' => 1,
'annual-extra' => 2,
'annual-preparation' => 3,
'annual-fraction' => 4,
'annual-training' => 5,
'annual-leave' => 6,
'annual-other-absence' => 7,
'annual-absence' => 8,
'annual-jnt' => 9,
];
return ($priority[$candidate] ?? 0) >= ($priority[$current] ?? 0) ? $candidate : $current;
}
private function cellDisplay(int $minutes, array $codes): string
{
if ($minutes <= 0) {
return '';
}
if ($codes !== []) {
return implode('/', $codes);
}
return $this->formatCompactMinutes($minutes);
}
private function vacationForDate(string $date, array $vacations): ?array
{
foreach ($vacations as $vacation) {
if ($date >= (string) $vacation['date_debut'] && $date <= (string) $vacation['date_fin']) {
return $vacation;
}
}
return null;
}
private function minutesBetween(string $start, string $end): int
{
[$startHour, $startMinute] = array_map('intval', explode(':', $start));
[$endHour, $endMinute] = array_map('intval', explode(':', $end));
return max(0, ($endHour * 60 + $endMinute) - ($startHour * 60 + $startMinute));
}
private function formatTotals(array $totals): array
{
$formatted = [];
foreach ($totals as $key => $minutes) {
$formatted[$key] = [
'minutes' => (int) $minutes,
'duration' => $this->formatMinutes((int) $minutes),
'compact' => $this->formatCompactMinutes((int) $minutes),
];
}
return $formatted;
}
private function formatMinutes(int $minutes): string
{
$sign = $minutes < 0 ? '-' : '';
$minutes = abs($minutes);
return sprintf('%s%d h %02d', $sign, intdiv($minutes, 60), $minutes % 60);
}
private function formatCompactMinutes(int $minutes): string
{
if ($minutes <= 0) {
return '';
}
$hours = intdiv($minutes, 60);
$mins = $minutes % 60;
return $mins === 0 ? $hours . 'h' : sprintf('%dh%02d', $hours, $mins);
}
private function monthName(int $month): string
{
return [
1 => 'Janvier', 2 => 'Février', 3 => 'Mars', 4 => 'Avril',
5 => 'Mai', 6 => 'Juin', 7 => 'Juillet', 8 => 'Août',
9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Décembre',
][$month];
}
private function weekdayShort(int $weekday): string
{
return [1 => 'L', 2 => 'M', 3 => 'M', 4 => 'J', 5 => 'V', 6 => 'S', 7 => 'D'][$weekday];
}
}