pour prod
This commit is contained in:
233
app/Controllers/CoverageController.php
Normal file
233
app/Controllers/CoverageController.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Access;
|
||||
use App\Core\Csrf;
|
||||
use App\Core\JsonResponse;
|
||||
use App\Core\Request;
|
||||
use App\Models\CoverageModel;
|
||||
use App\Models\StructureModel;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class CoverageController
|
||||
{
|
||||
private CoverageModel $coverageModel;
|
||||
private StructureModel $structureModel;
|
||||
private Access $access;
|
||||
|
||||
public function __construct(private PDO $pdo)
|
||||
{
|
||||
$this->coverageModel = new CoverageModel($pdo);
|
||||
$this->structureModel = new StructureModel($pdo);
|
||||
$this->access = new Access($pdo);
|
||||
}
|
||||
|
||||
public function alerts(): void
|
||||
{
|
||||
$this->access->requireRoles([Access::ROLE_RESPONSABLE, Access::ROLE_SERVICE]);
|
||||
$weekValue = trim((string) ($_GET['week'] ?? ''));
|
||||
$week = $this->parseWeek($weekValue);
|
||||
$structureId = filter_input(INPUT_GET, 'structure_id', FILTER_VALIDATE_INT);
|
||||
if ($this->access->role() === Access::ROLE_RESPONSABLE) {
|
||||
$structureId = $this->access->structureId();
|
||||
}
|
||||
|
||||
if ($week === null) {
|
||||
JsonResponse::send(400, ['error' => 'Semaine invalide.']);
|
||||
}
|
||||
|
||||
[$year, $weekNumber] = $week;
|
||||
$structures = $this->structureModel->allActive();
|
||||
if ($structureId) {
|
||||
$structures = array_values(array_filter(
|
||||
$structures,
|
||||
static fn (array $structure): bool => (int) $structure['id_structure'] === (int) $structureId
|
||||
));
|
||||
}
|
||||
|
||||
$alerts = [];
|
||||
$validationWarnings = [];
|
||||
$unconfigured = [];
|
||||
$affectedStructures = [];
|
||||
$gapMinutes = 0;
|
||||
|
||||
foreach ($structures as $structure) {
|
||||
$id = (int) $structure['id_structure'];
|
||||
$report = $this->coverageModel->reportForStructure($id, $year, $weekNumber);
|
||||
|
||||
if (!$report['configured']) {
|
||||
$unconfigured[] = [
|
||||
'id_structure' => $id,
|
||||
'nom' => $structure['nom'],
|
||||
'code' => $structure['code'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($report['gaps'] as $gap) {
|
||||
$gap['id_structure'] = $id;
|
||||
$gap['structure_nom'] = $structure['nom'];
|
||||
$gap['structure_code'] = $structure['code'];
|
||||
$gap['structure_type_affectation'] = $structure['type_affectation'];
|
||||
$alerts[] = $gap;
|
||||
$affectedStructures[$id] = true;
|
||||
$gapMinutes += $this->durationMinutes($gap['heure_debut'], $gap['heure_fin']);
|
||||
}
|
||||
|
||||
foreach ($report['validation_warnings'] as $warning) {
|
||||
$warning['id_structure'] = $id;
|
||||
$warning['structure_nom'] = $structure['nom'];
|
||||
$validationWarnings[] = $warning;
|
||||
}
|
||||
}
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'week' => $weekValue,
|
||||
'alerts' => $alerts,
|
||||
'validation_warnings' => $validationWarnings,
|
||||
'unconfigured_structures' => $unconfigured,
|
||||
'summary' => [
|
||||
'gap_count' => count($alerts),
|
||||
'affected_structure_count' => count($affectedStructures),
|
||||
'gap_minutes' => $gapMinutes,
|
||||
'validation_warning_count' => count($validationWarnings),
|
||||
'unconfigured_structure_count' => count($unconfigured),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): void
|
||||
{
|
||||
$this->access->requireService();
|
||||
$structureId = filter_input(INPUT_GET, 'structure_id', FILTER_VALIDATE_INT);
|
||||
if (!$structureId) {
|
||||
JsonResponse::send(422, ['error' => 'Lieu d’affectation invalide.']);
|
||||
}
|
||||
|
||||
$structure = $this->structureModel->findActive((int) $structureId);
|
||||
if ($structure === null) {
|
||||
JsonResponse::send(404, ['error' => 'Lieu d’affectation introuvable.']);
|
||||
}
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'structure' => $structure,
|
||||
'rules' => $this->coverageModel->rulesForStructure((int) $structureId),
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveRules(): void
|
||||
{
|
||||
$this->access->requireService();
|
||||
Request::requireMethod('POST');
|
||||
$payload = Request::json();
|
||||
Csrf::assertPayload($payload);
|
||||
|
||||
$structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT);
|
||||
$rawRules = $payload['rules'] ?? null;
|
||||
|
||||
if (!$structureId || !is_array($rawRules)) {
|
||||
JsonResponse::send(422, ['error' => 'Paramètres de couverture invalides.']);
|
||||
}
|
||||
if ($this->structureModel->findActive((int) $structureId) === null) {
|
||||
JsonResponse::send(404, ['error' => 'Lieu d’affectation introuvable.']);
|
||||
}
|
||||
|
||||
$rules = [];
|
||||
foreach ($rawRules as $index => $rawRule) {
|
||||
if (!is_array($rawRule)) {
|
||||
JsonResponse::send(422, ['error' => 'Une règle de couverture est invalide.']);
|
||||
}
|
||||
|
||||
$day = filter_var($rawRule['jour_semaine'] ?? null, FILTER_VALIDATE_INT);
|
||||
$start = trim((string) ($rawRule['heure_debut'] ?? ''));
|
||||
$end = trim((string) ($rawRule['heure_fin'] ?? ''));
|
||||
$minimum = filter_var($rawRule['minimum_agents'] ?? 1, FILTER_VALIDATE_INT);
|
||||
|
||||
if (!$day || $day < 1 || $day > 5 || !$this->validQuarterHour($start) || !$this->validQuarterHour($end)) {
|
||||
JsonResponse::send(422, ['error' => sprintf('La règle de couverture n°%d contient un jour ou un horaire invalide.', $index + 1)]);
|
||||
}
|
||||
if ($this->timeToMinutes($end) <= $this->timeToMinutes($start)) {
|
||||
JsonResponse::send(422, ['error' => sprintf('La fin doit être postérieure au début pour la règle n°%d.', $index + 1)]);
|
||||
}
|
||||
if (!$minimum || $minimum < 1 || $minimum > 50) {
|
||||
JsonResponse::send(422, ['error' => 'Le nombre minimum d’agents doit être compris entre 1 et 50.']);
|
||||
}
|
||||
|
||||
$rules[] = [
|
||||
'jour_semaine' => (int) $day,
|
||||
'heure_debut' => $start,
|
||||
'heure_fin' => $end,
|
||||
'minimum_agents' => (int) $minimum,
|
||||
];
|
||||
}
|
||||
|
||||
$this->assertNoOverlap($rules);
|
||||
|
||||
try {
|
||||
$this->coverageModel->replaceRules((int) $structureId, $rules);
|
||||
} catch (Throwable $e) {
|
||||
JsonResponse::send(500, [
|
||||
'error' => 'Impossible d’enregistrer les horaires de couverture.',
|
||||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'message' => 'Les horaires de couverture du lieu ont été enregistrés.',
|
||||
'rules' => $this->coverageModel->rulesForStructure((int) $structureId),
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertNoOverlap(array $rules): void
|
||||
{
|
||||
$byDay = [];
|
||||
foreach ($rules as $rule) {
|
||||
$byDay[(int) $rule['jour_semaine']][] = $rule;
|
||||
}
|
||||
|
||||
foreach ($byDay as $day => $dayRules) {
|
||||
usort($dayRules, fn (array $a, array $b): int => $this->timeToMinutes($a['heure_debut']) <=> $this->timeToMinutes($b['heure_debut']));
|
||||
$previousEnd = null;
|
||||
foreach ($dayRules as $rule) {
|
||||
$start = $this->timeToMinutes($rule['heure_debut']);
|
||||
$end = $this->timeToMinutes($rule['heure_fin']);
|
||||
if ($previousEnd !== null && $start < $previousEnd) {
|
||||
JsonResponse::send(422, ['error' => sprintf('Deux plages de couverture se chevauchent pour le jour %d.', $day)]);
|
||||
}
|
||||
$previousEnd = $end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validQuarterHour(string $time): bool
|
||||
{
|
||||
if (!preg_match('/^(?:[01]\d|2[0-3]):(?:00|15|30|45)$/', $time)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function timeToMinutes(string $time): int
|
||||
{
|
||||
[$hours, $minutes] = array_map('intval', explode(':', $time));
|
||||
return ($hours * 60) + $minutes;
|
||||
}
|
||||
|
||||
private function durationMinutes(string $start, string $end): int
|
||||
{
|
||||
return max(0, $this->timeToMinutes($end) - $this->timeToMinutes($start));
|
||||
}
|
||||
|
||||
private function parseWeek(string $value): ?array
|
||||
{
|
||||
if (!preg_match('/^(\d{4})-W(\d{2})$/', $value, $matches)) {
|
||||
return null;
|
||||
}
|
||||
$week = (int) $matches[2];
|
||||
return $week >= 1 && $week <= 53 ? [(int) $matches[1], $week] : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user