pour prod
This commit is contained in:
225
app/Models/CoverageModel.php
Normal file
225
app/Models/CoverageModel.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user