Ajout des commentaires dans le code

This commit is contained in:
PTA
2026-08-20 13:22:49 +02:00
parent 4201894e28
commit 5d8c0993a6
154 changed files with 5071 additions and 47 deletions

View File

@@ -8,6 +8,23 @@ use DateTimeImmutable;
use PDO;
use Throwable;
/**
* Règles de couverture d'un lieu et détection des plages sous-effectif.
*
* PRINCIPE DU CALCUL : chaque règle (jour, plage, minimum d'agents) est
* découpée en créneaux de 15 MINUTES, l'effectif présent est compté sur chacun,
* puis les créneaux consécutifs identiques sont refusionnés en intervalles
* lisibles. Ce détour par le quart d'heure est ce qui permet de traiter
* correctement des créneaux qui se recouvrent partiellement — un simple
* comptage par plage donnerait un effectif faux dès que deux agents ont des
* horaires décalés. Le pas de 15 minutes est aussi la granularité de saisie de
* toute l'application.
*
* Deux niveaux de signalement :
* - `gaps` : effectif PLANIFIÉ insuffisant — un vrai manque ;
* - `validation_warnings` : effectif atteint, mais pas encore VALIDÉ. La
* couverture n'est donc pas garantie, sans être en défaut.
*/
final class CoverageModel extends BaseModel
{
private const DAY_LABELS = [
@@ -18,6 +35,11 @@ final class CoverageModel extends BaseModel
5 => 'Vendredi',
];
/**
* Règles de couverture d'un lieu, triées par jour puis par horaire.
*
* @return array<int, array<string, mixed>>
*/
public function rulesForStructure(int $structureId): array
{
$stmt = $this->pdo->prepare(
@@ -33,6 +55,16 @@ final class CoverageModel extends BaseModel
return $stmt->fetchAll();
}
/**
* Remplace INTÉGRALEMENT les règles d'un lieu, en une transaction.
*
* Une liste vide supprime toute règle : le lieu redevient « non configuré »
* et cesse de produire des alertes. Il n'y a pas d'ajout ni de suppression
* unitaire, la validation métier (chevauchements compris) étant faite en
* amont par CoverageController::saveRules().
*
* @param array<int, array<string, mixed>> $rules Règles déjà validées.
*/
public function replaceRules(int $structureId, array $rules): void
{
try {
@@ -71,6 +103,18 @@ final class CoverageModel extends BaseModel
}
}
/**
* Rapport de couverture d'un lieu sur une semaine.
*
* Un lieu sans règle renvoie `configured: false` et AUCUNE alerte : il n'y a
* pas d'attendu, donc pas de manque. L'appelant doit le signaler à part —
* taire ces lieux les ferait passer pour conformes.
*
* L'analyse s'arrête au VENDREDI (weekStart + 4 jours), conformément au
* modèle métier.
*
* @return array{configured: bool, rules: array<int, array<string, mixed>>, gaps: array<int, array<string, mixed>>, validation_warnings: array<int, array<string, mixed>>, summary: array{gap_count: int, gap_minutes: int, validation_warning_count: int}}
*/
public function reportForStructure(int $structureId, int $year, int $weekNumber): array
{
$rules = $this->rulesForStructure($structureId);
@@ -172,6 +216,21 @@ final class CoverageModel extends BaseModel
];
}
/**
* Créneaux comptant pour la couverture d'un lieu.
*
* ATTENTION : le filtre porte sur le seul motif `TRAVAIL`. Les motifs de
* travail plus fins (ALP_MATIN, RESTAURATION, ALP_SOIR, EXTRASCOLAIRE…),
* pourtant reconnus comme du travail effectif par
* App\Core\MotifCategories::WORK_CODES, ne sont PAS comptés ici. Un lieu
* planifié exclusivement avec ces motifs apparaîtra donc entièrement
* découvert. À revoir si les alertes remontent des manques inexpliqués.
*
* Les agents désactivés sont exclus (jointure sur `a.actif = TRUE`) : ils ne
* couvrent plus rien.
*
* @return array<int, array<string, mixed>>
*/
private function workEntries(int $structureId, string $dateStart, string $dateEnd): array
{
$stmt = $this->pdo->prepare(
@@ -195,6 +254,21 @@ final class CoverageModel extends BaseModel
return $stmt->fetchAll();
}
/**
* Ajoute un créneau de 15 minutes, en le FUSIONNANT avec le précédent quand
* c'est le même jour, qu'il enchaîne exactement (fin = début) et que tous
* les compteurs comparés sont identiques.
*
* Sans cette fusion, une matinée découverte produirait seize alertes de
* quinze minutes au lieu d'une seule ligne de 8h00 à 12h00.
*
* Ne regarde que le DERNIER élément : les créneaux sont produits dans
* l'ordre chronologique, une fusion plus lointaine n'a pas lieu d'être.
*
* @param array<int, array<string, mixed>> $target Liste enrichie sur place.
* @param array<string, mixed> $slot Créneau candidat.
* @param array<int, string> $comparisonKeys Compteurs devant coïncider.
*/
private function appendMergedInterval(array &$target, array $slot, array $comparisonKeys): void
{
$lastIndex = count($target) - 1;
@@ -212,12 +286,14 @@ final class CoverageModel extends BaseModel
$target[] = $slot;
}
/** « 09:30 » → 570. Tronque à HH:MM, les horaires arrivant parfois en HH:MM:SS. */
private function timeToMinutes(string $time): int
{
[$hours, $minutes] = array_map('intval', explode(':', substr($time, 0, 5)));
return ($hours * 60) + $minutes;
}
/** 570 → « 09:30 ». Opération inverse de timeToMinutes(). */
private function minutesToTime(int $minutes): string
{
return sprintf('%02d:%02d', intdiv($minutes, 60), $minutes % 60);