pour prod
This commit is contained in:
642
app/Models/PtaModel.php
Normal file
642
app/Models/PtaModel.php
Normal 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 d’au 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 d’entretien 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 l’enveloppe 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 l’agent 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 l’agent, 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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user