226 lines
8.2 KiB
PHP
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);
|
|
}
|
|
}
|