Files
pta/app/Models/AnnualOverviewModel.php
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

343 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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];
}
}