Ajout des commentaires dans le code
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user