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

642
app/Models/PtaModel.php Normal file
View File

@@ -0,0 +1,642 @@
<?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];
}
}