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

643 lines
28 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 PDO;
use RuntimeException;
final class PtaModel extends BaseModel
{
private const WORK_CODES = [
'TRAVAIL', 'ALP_MATIN', 'RESTAURATION', 'ALP_SOIR',
'MERCREDI_PERISCOLAIRE', 'EXTRASCOLAIRE',
'PREPARATION_PERISCOLAIRE', 'PREPARATION_EXTRASCOLAIRE',
'PREPARATION_LUNDI', 'MENAGE_FOND', 'JOUR_FRACTIONNEMENT',
];
private const PREPARATION_CODES = [
'PREPARATION_PERISCOLAIRE', 'PREPARATION_EXTRASCOLAIRE', 'PREPARATION_LUNDI',
];
public function summary(int $agentId, string $contextDate): array
{
$agentModel = new AgentModel($this->pdo);
$agent = $agentModel->findActive($agentId);
if ($agent === null) {
throw new RuntimeException('Agent introuvable.');
}
$quota = (new QuotaModel($this->pdo))->forDateForApi($agentId, $contextDate);
$pta = $this->ensurePta($agent, $quota);
$entries = $agentModel->entriesForDateRange(
$agentId,
(string) $quota['date_debut_periode'],
(string) $quota['date_fin_periode']
);
$vacations = (new VacationModel($this->pdo))->periodsBetween(
(string) $quota['date_debut_periode'],
(string) $quota['date_fin_periode']
);
$analysis = $this->analyseEntries($agent, $entries, $vacations, $quota);
$constraints = $this->constraintsForAgent(
$agentId,
(string) $quota['date_debut_periode'],
(string) $quota['date_fin_periode']
);
$wishes = $this->wishesForAgent($agentId);
$controls = array_merge(
$this->eligibilityControls($agent),
$analysis['controls'],
$this->constraintControls($analysis, $constraints),
$this->deepCleaningControls($agentId, (string) $quota['date_debut_periode'], (string) $quota['date_fin_periode'])
);
usort($controls, static function (array $a, array $b): int {
$order = ['ERREUR' => 0, 'ALERTE' => 1, 'INFO' => 2];
return ($order[$a['niveau']] ?? 9) <=> ($order[$b['niveau']] ?? 9);
});
return [
'agent' => $agent,
'pta' => $pta,
'quota' => $quota,
'categories' => $analysis['categories'],
'counters' => $analysis['counters'],
'controls' => $controls,
'constraints' => $constraints,
'wishes' => $wishes,
'vacations' => $vacations,
];
}
public function saveConstraint(array $data): int
{
$stmt = $this->pdo->prepare(
"INSERT INTO agent_contrainte (
id_agent, type_contrainte, date_debut, date_fin,
quotite_temporaire, maximum_minutes_jour, maximum_minutes_semaine,
interdit_matin, interdit_midi, interdit_soir, commentaire, actif
) VALUES (
:id_agent, :type_contrainte, :date_debut, :date_fin,
:quotite_temporaire, :maximum_minutes_jour, :maximum_minutes_semaine,
:interdit_matin, :interdit_midi, :interdit_soir, :commentaire, TRUE
)"
);
$stmt->execute($data);
return (int) $this->pdo->lastInsertId();
}
public function deleteConstraint(int $constraintId, int $agentId): bool
{
$stmt = $this->pdo->prepare(
'DELETE FROM agent_contrainte WHERE id_contrainte = :id AND id_agent = :agent_id'
);
$stmt->execute(['id' => $constraintId, 'agent_id' => $agentId]);
return $stmt->rowCount() > 0;
}
public function saveWish(array $data): int
{
$stmt = $this->pdo->prepare(
"INSERT INTO agent_structure_souhait (
id_agent, id_structure, type_souhait, priorite, distance_km, commentaire, actif
) VALUES (
:id_agent, :id_structure, :type_souhait, :priorite, :distance_km, :commentaire, TRUE
)
ON DUPLICATE KEY UPDATE
type_souhait = VALUES(type_souhait),
priorite = VALUES(priorite),
distance_km = VALUES(distance_km),
commentaire = VALUES(commentaire),
actif = TRUE"
);
$stmt->execute($data);
return (int) ($this->pdo->lastInsertId() ?: 0);
}
public function deleteWish(int $wishId, int $agentId): bool
{
$stmt = $this->pdo->prepare(
'DELETE FROM agent_structure_souhait WHERE id_souhait = :id AND id_agent = :agent_id'
);
$stmt->execute(['id' => $wishId, 'agent_id' => $agentId]);
return $stmt->rowCount() > 0;
}
private function ensurePta(array $agent, array $quota): array
{
$stmt = $this->pdo->prepare(
'SELECT * FROM pta_annuel
WHERE id_agent = :agent_id
AND date_debut = :date_debut
AND date_fin = :date_fin
LIMIT 1'
);
$stmt->execute([
'agent_id' => $agent['id_agent'],
'date_debut' => $quota['date_debut_periode'],
'date_fin' => $quota['date_fin_periode'],
]);
$pta = $stmt->fetch();
if ($pta) {
return $pta;
}
$training = !empty($agent['formation_repartition_annuelle']) ? 0 : 840;
$insert = $this->pdo->prepare(
"INSERT INTO pta_annuel (
id_agent, date_debut, date_fin, quotite_travail,
quota_reference_minutes, quota_cible_minutes,
enveloppe_formation_minutes, statut
) VALUES (
:agent_id, :date_debut, :date_fin, :quotite,
:quota_reference, :quota_cible, :formation, 'BROUILLON'
)"
);
$insert->execute([
'agent_id' => $agent['id_agent'],
'date_debut' => $quota['date_debut_periode'],
'date_fin' => $quota['date_fin_periode'],
'quotite' => $quota['quotite_travail'],
'quota_reference' => $quota['quota_reference_minutes'],
'quota_cible' => $quota['quota_cible_minutes'],
'formation' => $training,
]);
$stmt->execute([
'agent_id' => $agent['id_agent'],
'date_debut' => $quota['date_debut_periode'],
'date_fin' => $quota['date_fin_periode'],
]);
return $stmt->fetch() ?: [];
}
private function analyseEntries(array $agent, array $entries, array $vacations, array $quota): array
{
$categories = [
'PERISCOLAIRE' => 0,
'MERCREDI_PERISCOLAIRE' => 0,
'EXTRASCOLAIRE' => 0,
'FORMATION' => 0,
'CONGE' => 0,
'JOUR_FRACTIONNEMENT' => 0,
'PREPARATION' => 0,
'MENAGE_FOND' => 0,
'ABSENCE' => 0,
'NON_DECOMPTE' => 0,
];
$daysByCategory = [];
$daily = [];
$weekly = [];
$controls = [];
foreach ($entries as $entry) {
$date = (string) $entry['date_jour'];
$start = (string) $entry['heure_debut'];
$end = (string) $entry['heure_fin'];
$minutes = $this->minutesBetween($start, $end);
$code = strtoupper((string) $entry['motif_code']);
$isVacation = $this->vacationForDate($date, $vacations) !== null;
$weekday = (int) (new DateTimeImmutable($date))->format('N');
$category = $this->categoryForEntry($code, $isVacation, $weekday, (bool) $entry['compte_dans_quota']);
$categories[$category] = ($categories[$category] ?? 0) + $minutes;
$daysByCategory[$category][$date] = true;
$daily[$date] ??= ['minutes' => 0, 'starts' => [], 'ends' => [], 'entries' => []];
$daily[$date]['minutes'] += $minutes;
$daily[$date]['starts'][] = $start;
$daily[$date]['ends'][] = $end;
$daily[$date]['entries'][] = [
'start' => $start,
'end' => $end,
'minutes' => $minutes,
'code' => $code,
'category' => $category,
];
$monday = (new DateTimeImmutable($date))->modify('-' . ($weekday - 1) . ' days')->format('Y-m-d');
$weekly[$monday] = ($weekly[$monday] ?? 0) + $minutes;
}
foreach ($daily as $date => &$day) {
sort($day['starts']);
sort($day['ends']);
usort($day['entries'], static fn(array $a, array $b): int => strcmp($a['start'], $b['start']));
$first = min($day['starts']);
$last = max($day['ends']);
$day['amplitude_minutes'] = $this->minutesBetween($first, $last);
$day['first'] = $first;
$day['last'] = $last;
}
unset($day);
foreach ($daily as $date => $day) {
if ($day['minutes'] > 600) {
$controls[] = $this->control('ERREUR', 'DUREE_QUOTIDIENNE', sprintf(
'%s : %s planifiées, au-delà de 10 h de travail quotidien.',
$this->formatDate($date), $this->formatMinutes($day['minutes'])
));
}
if ($day['amplitude_minutes'] > 720) {
$controls[] = $this->control('ERREUR', 'AMPLITUDE_QUOTIDIENNE', sprintf(
'%s : amplitude de %s, au-delà de 12 h.',
$this->formatDate($date), $this->formatMinutes($day['amplitude_minutes'])
));
}
$continuous = 0;
$previousEnd = null;
foreach ($day['entries'] as $entry) {
if ($previousEnd === null || $this->minutesBetween($previousEnd, $entry['start']) >= 20) {
$continuous = $entry['minutes'];
} else {
$continuous += max(0, $this->minutesBetween(max($previousEnd, $entry['start']), $entry['end']));
}
$previousEnd = $previousEnd === null || $entry['end'] > $previousEnd ? $entry['end'] : $previousEnd;
if ($continuous > 360) {
$controls[] = $this->control('ERREUR', 'PAUSE_OBLIGATOIRE', sprintf(
'%s : plus de 6 h consécutives sans coupure dau moins 20 minutes.',
$this->formatDate($date)
));
break;
}
}
}
$dates = array_keys($daily);
sort($dates);
for ($i = 1, $count = count($dates); $i < $count; $i++) {
$previousDate = $dates[$i - 1];
$currentDate = $dates[$i];
$previousEnd = new DateTimeImmutable($previousDate . ' ' . $daily[$previousDate]['last']);
$currentStart = new DateTimeImmutable($currentDate . ' ' . $daily[$currentDate]['first']);
$restMinutes = (int) round(($currentStart->getTimestamp() - $previousEnd->getTimestamp()) / 60);
if ($restMinutes < 660) {
$controls[] = $this->control('ERREUR', 'REPOS_QUOTIDIEN', sprintf(
'Repos insuffisant entre le %s et le %s : %s au lieu de 11 h minimum.',
$this->formatDate($previousDate), $this->formatDate($currentDate), $this->formatMinutes($restMinutes)
));
}
}
foreach ($weekly as $monday => $minutes) {
if ($minutes > 2880) {
$controls[] = $this->control('ERREUR', 'DUREE_HEBDOMADAIRE', sprintf(
'Semaine du %s : %s planifiées, au-delà de 48 h.',
$this->formatDate($monday), $this->formatMinutes($minutes)
));
}
}
// Contrôle de la moyenne maximale de 44 h sur 12 semaines consécutives.
$periodStart = new DateTimeImmutable((string) $quota['date_debut_periode']);
$periodEnd = new DateTimeImmutable((string) $quota['date_fin_periode']);
$firstMonday = $periodStart->modify('-' . ((int) $periodStart->format('N') - 1) . ' days');
$lastMonday = $periodEnd->modify('-' . ((int) $periodEnd->format('N') - 1) . ' days');
$weekSeries = [];
for ($monday = $firstMonday; $monday <= $lastMonday; $monday = $monday->modify('+7 days')) {
$key = $monday->format('Y-m-d');
$weekSeries[] = ['date' => $key, 'minutes' => (int) ($weekly[$key] ?? 0)];
}
for ($index = 0, $totalWeeks = count($weekSeries); $index + 11 < $totalWeeks; $index++) {
$window = array_slice($weekSeries, $index, 12);
$average = (int) round(array_sum(array_column($window, 'minutes')) / 12);
if ($average > 2640) {
$controls[] = $this->control('ERREUR', 'MOYENNE_12_SEMAINES', sprintf(
'Du %s au %s : moyenne de %s par semaine sur 12 semaines, au-delà de 44 h.',
$this->formatDate($window[0]['date']),
$this->formatDate((new DateTimeImmutable($window[11]['date']))->modify('+6 days')->format('Y-m-d')),
$this->formatMinutes($average)
));
break;
}
}
$expectedWednesday = (int) ($agent['heures_mercredi_minutes'] ?? 0);
$expectedExtra = (int) ($agent['heures_extrascolaire_minutes'] ?? 0);
$vacationWeeks = [];
foreach ($daily as $date => $day) {
$dateObject = new DateTimeImmutable($date);
$weekday = (int) $dateObject->format('N');
$vacation = $this->vacationForDate($date, $vacations);
$workingMinutes = 0;
foreach ($day['entries'] as $entry) {
if (in_array($entry['code'], self::WORK_CODES, true)) {
$workingMinutes += $entry['minutes'];
}
}
if ($weekday === 3 && $vacation === null && $workingMinutes > 0 && $expectedWednesday > 0 && $workingMinutes !== $expectedWednesday) {
$controls[] = $this->control('ALERTE', 'MERCREDI_DUREE', sprintf(
'%s : %s de travail le mercredi scolaire, contre %s attendues pour le poste.',
$this->formatDate($date), $this->formatMinutes($workingMinutes), $this->formatMinutes($expectedWednesday)
));
}
if ($vacation !== null && $workingMinutes > 0 && $expectedExtra > 0 && $workingMinutes !== $expectedExtra) {
$controls[] = $this->control('ALERTE', 'EXTRA_DUREE', sprintf(
'%s : %s de travail extrascolaire, contre %s attendues pour le poste.',
$this->formatDate($date), $this->formatMinutes($workingMinutes), $this->formatMinutes($expectedExtra)
));
}
if ($vacation !== null && $workingMinutes > 0) {
$weekKey = $dateObject->modify('-' . ($weekday - 1) . ' days')->format('Y-m-d');
$vacationWeeks[$weekKey][$date] = true;
}
}
if (($agent['poste_famille'] ?? null) === 'ANIMATION') {
foreach ($vacationWeeks as $weekStart => $workedDays) {
$count = count($workedDays);
if ($count > 0 && $count < 5) {
$controls[] = $this->control('ALERTE', 'VACANCES_SEMAINE_INCOMPLETE', sprintf(
'Semaine de vacances du %s : seulement %d jour(s) travaillé(s). La préférence métier est une semaine complète.',
$this->formatDate($weekStart), $count
));
}
}
}
if (($agent['poste_famille'] ?? null) === 'RESTAURATION_ENTRETIEN') {
foreach (self::PREPARATION_CODES as $code) {
$hasPreparation = false;
foreach ($entries as $entry) {
if (strtoupper((string) $entry['motif_code']) === $code) {
$hasPreparation = true;
break;
}
}
if ($hasPreparation) {
$controls[] = $this->control('ERREUR', 'PREPARATION_NON_AUTORISEE',
'Un agent de restauration collective et dentretien ne doit pas recevoir de temps de préparation.');
break;
}
}
}
$formationMinutes = $categories['FORMATION'];
$formationTarget = !empty($agent['formation_repartition_annuelle']) ? 0 : 840;
if ($formationTarget > 0 && $formationMinutes > $formationTarget) {
$controls[] = $this->control('ALERTE', 'FORMATION_DEPASSEE', sprintf(
'Formation : %s planifiées pour une enveloppe complémentaire de 14 h.',
$this->formatMinutes($formationMinutes)
));
}
if ($formationTarget > 0 && $formationMinutes < $formationTarget) {
$controls[] = $this->control('INFO', 'FORMATION_RESTANTE', sprintf(
'Il reste %s dans lenveloppe de formation CNFPT de 14 h.',
$this->formatMinutes($formationTarget - $formationMinutes)
));
}
$caDays = count($daysByCategory['CONGE'] ?? []);
$jfDays = count($daysByCategory['JOUR_FRACTIONNEMENT'] ?? []);
if ($caDays !== 25) {
$controls[] = $this->control('ALERTE', 'CONGES_ANNUELS', sprintf(
'%d jour(s) de congé annuel positionné(s) sur un objectif métier de 25 jours.',
$caDays
));
}
if ($jfDays !== 2) {
$controls[] = $this->control('ALERTE', 'JOURS_FRACTIONNEMENT', sprintf(
'%d journée(s) de fractionnement positionnée(s) sur un objectif de 2.',
$jfDays
));
}
if ($controls === []) {
$controls[] = $this->control('INFO', 'OK', 'Aucune anomalie détectée sur le PTA de cette période.');
}
$formattedCategories = [];
foreach ($categories as $code => $minutes) {
$formattedCategories[] = [
'code' => $code,
'minutes' => $minutes,
'libelle' => $this->categoryLabel($code),
'duree' => $this->formatMinutes($minutes),
'jours' => count($daysByCategory[$code] ?? []),
];
}
return [
'categories' => $formattedCategories,
'counters' => [
'conges_annuels_jours' => $caDays,
'conges_annuels_cible' => 25,
'jours_fractionnement' => $jfDays,
'jours_fractionnement_cible' => 2,
'formation_minutes' => $formationMinutes,
'formation_cible_minutes' => $formationTarget,
'formation_duree' => $this->formatMinutes($formationMinutes),
'formation_cible_duree' => $this->formatMinutes($formationTarget),
'quota_cible' => $quota['quota_cible'],
'heures_affectees' => $quota['affectees'],
'heures_restantes' => $quota['restantes'],
],
'controls' => $controls,
'daily' => $daily,
'weekly' => $weekly,
];
}
private function eligibilityControls(array $agent): array
{
$controls = [];
if (($agent['type_contrat'] ?? '') !== 'PERMANENT') {
$controls[] = $this->control('ERREUR', 'CONTRAT_NON_PERMANENT',
'Le dispositif PTA est réservé ici aux agents en contrat permanent.');
}
if (empty($agent['id_poste'])) {
$controls[] = $this->control('ERREUR', 'POSTE_MANQUANT',
'Le poste de lagent doit être renseigné pour appliquer les règles du PTA.');
}
return $controls;
}
private function constraintControls(array $analysis, array $constraints): array
{
$controls = [];
foreach ($constraints as $constraint) {
$label = $constraint['type_contrainte'] === 'TEMPS_PARTIEL_THERAPEUTIQUE'
? 'Temps partiel thérapeutique'
: 'Préconisation médicale';
$controls[] = $this->control('ALERTE', 'CONTRAINTE_PRIORITAIRE', sprintf(
'%s du %s au %s : %s',
$label,
$this->formatDate((string) $constraint['date_debut']),
$constraint['date_fin'] ? $this->formatDate((string) $constraint['date_fin']) : 'sans date de fin',
(string) $constraint['commentaire']
));
foreach ($analysis['daily'] as $date => $day) {
if ($date < $constraint['date_debut'] || ($constraint['date_fin'] && $date > $constraint['date_fin'])) {
continue;
}
if ($constraint['maximum_minutes_jour'] !== null
&& $day['minutes'] > (int) $constraint['maximum_minutes_jour']) {
$controls[] = $this->control('ERREUR', 'CONTRAINTE_MAX_JOUR', sprintf(
'%s : %s planifiées alors que la contrainte limite la journée à %s.',
$this->formatDate($date), $this->formatMinutes($day['minutes']),
$this->formatMinutes((int) $constraint['maximum_minutes_jour'])
));
}
}
}
return $controls;
}
private function deepCleaningControls(int $agentId, string $startDate, string $endDate): array
{
$stmt = $this->pdo->prepare(
"SELECT c.date_jour, p.id_structure, s.nom AS structure_nom,
COUNT(DISTINCT p.id_agent) AS agents,
SUM(CASE WHEN p.id_agent = :agent_id THEN TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin)) ELSE 0 END) AS minutes_agent
FROM creneau_horaire c
JOIN planning p ON p.id_planning = c.id_planning
JOIN motif_planning m ON m.id_motif = c.id_motif
JOIN structure s ON s.id_structure = p.id_structure
WHERE m.code = 'MENAGE_FOND'
AND c.date_jour BETWEEN :date_debut AND :date_fin
AND EXISTS (
SELECT 1 FROM planning px
JOIN creneau_horaire cx ON cx.id_planning = px.id_planning
JOIN motif_planning mx ON mx.id_motif = cx.id_motif
WHERE px.id_agent = :agent_exists
AND px.id_structure = p.id_structure
AND cx.date_jour = c.date_jour
AND mx.code = 'MENAGE_FOND'
)
GROUP BY c.date_jour, p.id_structure, s.nom"
);
$stmt->execute([
'agent_id' => $agentId,
'agent_exists' => $agentId,
'date_debut' => $startDate,
'date_fin' => $endDate,
]);
$controls = [];
foreach ($stmt->fetchAll() as $row) {
if ((int) $row['agents'] < 2) {
$controls[] = $this->control('ERREUR', 'MENAGE_EQUIPE', sprintf(
'%s — %s : le ménage de fond ne compte que %d agent(s), alors que 2 sont requis.',
$this->formatDate((string) $row['date_jour']), (string) $row['structure_nom'], (int) $row['agents']
));
}
if ((int) $row['minutes_agent'] !== 420) {
$controls[] = $this->control('ALERTE', 'MENAGE_DUREE', sprintf(
'%s — %s : %s de ménage de fond pour lagent, au lieu de 7 h.',
$this->formatDate((string) $row['date_jour']), (string) $row['structure_nom'],
$this->formatMinutes((int) $row['minutes_agent'])
));
}
}
return $controls;
}
private function constraintsForAgent(int $agentId, string $startDate, string $endDate): array
{
$stmt = $this->pdo->prepare(
"SELECT id_contrainte, type_contrainte, date_debut, date_fin,
quotite_temporaire, maximum_minutes_jour, maximum_minutes_semaine,
interdit_matin, interdit_midi, interdit_soir, commentaire
FROM agent_contrainte
WHERE id_agent = :agent_id
AND actif = TRUE
AND date_debut <= :date_fin
AND COALESCE(date_fin, '9999-12-31') >= :date_debut
ORDER BY date_debut DESC"
);
$stmt->execute(['agent_id' => $agentId, 'date_debut' => $startDate, 'date_fin' => $endDate]);
return $stmt->fetchAll();
}
private function wishesForAgent(int $agentId): array
{
$stmt = $this->pdo->prepare(
"SELECT sw.id_souhait, sw.id_structure, s.nom AS structure_nom,
sw.type_souhait, sw.priorite, sw.distance_km, sw.commentaire
FROM agent_structure_souhait sw
JOIN structure s ON s.id_structure = sw.id_structure
WHERE sw.id_agent = :agent_id AND sw.actif = TRUE
ORDER BY FIELD(sw.type_souhait, 'INTERDICTION', 'PREFERENCE'), sw.priorite DESC, s.nom"
);
$stmt->execute(['agent_id' => $agentId]);
return $stmt->fetchAll();
}
private function categoryForEntry(string $code, bool $isVacation, int $weekday, bool $counts): string
{
return match ($code) {
'ALP_MATIN', 'RESTAURATION', 'ALP_SOIR' => 'PERISCOLAIRE',
'MERCREDI_PERISCOLAIRE' => 'MERCREDI_PERISCOLAIRE',
'EXTRASCOLAIRE' => 'EXTRASCOLAIRE',
'FORMATION' => 'FORMATION',
'CONGE' => 'CONGE',
'JOUR_FRACTIONNEMENT' => 'JOUR_FRACTIONNEMENT',
'PREPARATION_PERISCOLAIRE', 'PREPARATION_EXTRASCOLAIRE', 'PREPARATION_LUNDI' => 'PREPARATION',
'MENAGE_FOND' => 'MENAGE_FOND',
'ABSENCE' => 'ABSENCE',
'AUTRE_ABSENCE', 'JNT' => 'NON_DECOMPTE',
'TRAVAIL' => $isVacation ? 'EXTRASCOLAIRE' : ($weekday === 3 ? 'MERCREDI_PERISCOLAIRE' : 'PERISCOLAIRE'),
default => $counts ? 'PERISCOLAIRE' : 'NON_DECOMPTE',
};
}
private function categoryLabel(string $code): string
{
return match ($code) {
'PERISCOLAIRE' => 'Temps périscolaire lundi, mardi, jeudi et vendredi',
'MERCREDI_PERISCOLAIRE' => 'Mercredis périscolaires',
'EXTRASCOLAIRE' => 'Temps extrascolaire',
'FORMATION' => 'Formation',
'CONGE' => 'Congés annuels non décomptés',
'JOUR_FRACTIONNEMENT' => 'Journées de fractionnement',
'PREPARATION' => 'Temps de préparation',
'MENAGE_FOND' => 'Ménages de fond',
'ABSENCE' => 'Absences décomptées',
'NON_DECOMPTE' => 'JNT et absences non décomptées',
default => $code,
};
}
private function vacationForDate(string $date, array $vacations): ?array
{
foreach ($vacations as $vacation) {
if ($date >= $vacation['date_debut'] && $date <= $vacation['date_fin']) {
return $vacation;
}
}
return null;
}
private function minutesBetween(string $start, string $end): int
{
[$sh, $sm] = array_map('intval', explode(':', substr($start, 0, 5)));
[$eh, $em] = array_map('intval', explode(':', substr($end, 0, 5)));
return ($eh * 60 + $em) - ($sh * 60 + $sm);
}
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 formatDate(string $date): string
{
return (new DateTimeImmutable($date))->format('d/m/Y');
}
private function control(string $level, string $code, string $message): array
{
return ['niveau' => $level, 'code' => $code, 'message' => $message];
}
}