pour prod
This commit is contained in:
342
app/Models/AnnualOverviewModel.php
Normal file
342
app/Models/AnnualOverviewModel.php
Normal 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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user