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

226 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use DateTimeImmutable;
use PDO;
use Throwable;
final class CoverageModel extends BaseModel
{
private const DAY_LABELS = [
1 => 'Lundi',
2 => 'Mardi',
3 => 'Mercredi',
4 => 'Jeudi',
5 => 'Vendredi',
];
public function rulesForStructure(int $structureId): array
{
$stmt = $this->pdo->prepare(
"SELECT id_couverture, id_structure, jour_semaine,
TIME_FORMAT(heure_debut, '%H:%i') AS heure_debut,
TIME_FORMAT(heure_fin, '%H:%i') AS heure_fin,
minimum_agents
FROM structure_couverture_horaire
WHERE id_structure = :structure_id
ORDER BY jour_semaine, heure_debut, heure_fin"
);
$stmt->execute(['structure_id' => $structureId]);
return $stmt->fetchAll();
}
public function replaceRules(int $structureId, array $rules): void
{
try {
$this->pdo->beginTransaction();
$delete = $this->pdo->prepare(
'DELETE FROM structure_couverture_horaire WHERE id_structure = :structure_id'
);
$delete->execute(['structure_id' => $structureId]);
if ($rules !== []) {
$insert = $this->pdo->prepare(
'INSERT INTO structure_couverture_horaire
(id_structure, jour_semaine, heure_debut, heure_fin, minimum_agents)
VALUES
(:structure_id, :jour_semaine, :heure_debut, :heure_fin, :minimum_agents)'
);
foreach ($rules as $rule) {
$insert->execute([
'structure_id' => $structureId,
'jour_semaine' => (int) $rule['jour_semaine'],
'heure_debut' => $rule['heure_debut'],
'heure_fin' => $rule['heure_fin'],
'minimum_agents' => (int) $rule['minimum_agents'],
]);
}
}
$this->pdo->commit();
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
throw $e;
}
}
public function reportForStructure(int $structureId, int $year, int $weekNumber): array
{
$rules = $this->rulesForStructure($structureId);
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0);
$weekEnd = $weekStart->modify('+4 days');
if ($rules === []) {
return [
'configured' => false,
'rules' => [],
'gaps' => [],
'validation_warnings' => [],
'summary' => [
'gap_count' => 0,
'gap_minutes' => 0,
'validation_warning_count' => 0,
],
];
}
$entries = $this->workEntries(
$structureId,
$weekStart->format('Y-m-d'),
$weekEnd->format('Y-m-d')
);
$entriesByDate = [];
foreach ($entries as $entry) {
$entriesByDate[$entry['date_jour']][] = $entry;
}
$gaps = [];
$validationWarnings = [];
foreach ($rules as $rule) {
$dayNumber = (int) $rule['jour_semaine'];
$date = $weekStart->modify('+' . ($dayNumber - 1) . ' days')->format('Y-m-d');
$start = $this->timeToMinutes((string) $rule['heure_debut']);
$end = $this->timeToMinutes((string) $rule['heure_fin']);
$minimum = (int) $rule['minimum_agents'];
for ($slotStart = $start; $slotStart < $end; $slotStart += 15) {
$slotEnd = min($slotStart + 15, $end);
$plannedAgents = [];
$validatedAgents = [];
foreach ($entriesByDate[$date] ?? [] as $entry) {
$entryStart = $this->timeToMinutes((string) $entry['heure_debut']);
$entryEnd = $this->timeToMinutes((string) $entry['heure_fin']);
if ($entryStart >= $slotEnd || $entryEnd <= $slotStart) {
continue;
}
$agentId = (int) $entry['id_agent'];
$plannedAgents[$agentId] = true;
if ($entry['statut'] === 'VALIDE') {
$validatedAgents[$agentId] = true;
}
}
$plannedCount = count($plannedAgents);
$validatedCount = count($validatedAgents);
$base = [
'date' => $date,
'jour_semaine' => $dayNumber,
'jour_libelle' => self::DAY_LABELS[$dayNumber] ?? 'Jour',
'heure_debut' => $this->minutesToTime($slotStart),
'heure_fin' => $this->minutesToTime($slotEnd),
'minimum_agents' => $minimum,
'agents_planifies' => $plannedCount,
'agents_valides' => $validatedCount,
];
if ($plannedCount < $minimum) {
$base['agents_manquants'] = $minimum - $plannedCount;
$this->appendMergedInterval($gaps, $base, ['minimum_agents', 'agents_planifies', 'agents_manquants']);
} elseif ($validatedCount < $minimum) {
$base['validations_manquantes'] = $minimum - $validatedCount;
$this->appendMergedInterval($validationWarnings, $base, ['minimum_agents', 'agents_valides', 'validations_manquantes']);
}
}
}
$gapMinutes = 0;
foreach ($gaps as $gap) {
$gapMinutes += $this->timeToMinutes($gap['heure_fin']) - $this->timeToMinutes($gap['heure_debut']);
}
return [
'configured' => true,
'rules' => $rules,
'gaps' => $gaps,
'validation_warnings' => $validationWarnings,
'summary' => [
'gap_count' => count($gaps),
'gap_minutes' => $gapMinutes,
'validation_warning_count' => count($validationWarnings),
],
];
}
private function workEntries(int $structureId, string $dateStart, string $dateEnd): array
{
$stmt = $this->pdo->prepare(
"SELECT p.id_agent, p.statut, c.date_jour,
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin
FROM planning p
INNER JOIN agent a ON a.id_agent = p.id_agent AND a.actif = TRUE
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
INNER JOIN motif_planning m ON m.id_motif = c.id_motif
WHERE p.id_structure = :structure_id
AND c.date_jour BETWEEN :date_start AND :date_end
AND m.code = 'TRAVAIL'
ORDER BY c.date_jour, c.heure_debut, c.heure_fin"
);
$stmt->execute([
'structure_id' => $structureId,
'date_start' => $dateStart,
'date_end' => $dateEnd,
]);
return $stmt->fetchAll();
}
private function appendMergedInterval(array &$target, array $slot, array $comparisonKeys): void
{
$lastIndex = count($target) - 1;
if ($lastIndex >= 0) {
$last = $target[$lastIndex];
$same = $last['date'] === $slot['date'] && $last['heure_fin'] === $slot['heure_debut'];
foreach ($comparisonKeys as $key) {
$same = $same && (int) $last[$key] === (int) $slot[$key];
}
if ($same) {
$target[$lastIndex]['heure_fin'] = $slot['heure_fin'];
return;
}
}
$target[] = $slot;
}
private function timeToMinutes(string $time): int
{
[$hours, $minutes] = array_map('intval', explode(':', substr($time, 0, 5)));
return ($hours * 60) + $minutes;
}
private function minutesToTime(int $minutes): string
{
return sprintf('%02d:%02d', intdiv($minutes, 60), $minutes % 60);
}
}