Ajout des commentaires dans le code
This commit is contained in:
@@ -8,8 +8,42 @@ use DateTimeImmutable;
|
||||
use DomainException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Semaines types : modèles lundi→vendredi rattachés à un agent.
|
||||
*
|
||||
* Deux tables : `modele_semaine_agent` (l'en-tête, nom + agent) et
|
||||
* `modele_semaine_creneau` (les plages, repérées par un JOUR DE LA SEMAINE de 1
|
||||
* à 5 et non par une date). Un modèle ne produit aucun créneau tant qu'il n'est
|
||||
* pas appliqué.
|
||||
*
|
||||
* Deux voies d'entrée qui convergent sur persistTemplate() :
|
||||
* saveFromWeek() ← capture d'une semaine réellement planifiée ;
|
||||
* saveFromEntries() ← composition directe dans l'éditeur (V7.10).
|
||||
*
|
||||
* Deux voies de sortie :
|
||||
* applyToWeek() ← une semaine précise ;
|
||||
* applyToPeriod() ← toutes les semaines d'un intervalle, avec filtrage sur le
|
||||
* calendrier scolaire ; previewPeriod() en est la simulation
|
||||
* sans écriture, les deux partageant buildPeriodPlan().
|
||||
*
|
||||
* Conventions communes à tout le fichier :
|
||||
* - les heures sont manipulées en HH:MM:SS, format des colonnes TIME (l'éditeur
|
||||
* les tronque à HH:MM pour l'affichage seulement) ;
|
||||
* - les erreurs métier sont des DomainException dont le MESSAGE est destiné à
|
||||
* l'utilisateur final et affiché tel quel ;
|
||||
* - toute écriture est transactionnelle, et un planning validé touché repasse
|
||||
* en BROUILLON.
|
||||
*/
|
||||
final class WeekTemplateModel extends BaseModel
|
||||
{
|
||||
/**
|
||||
* Modèles d'un agent, avec leurs compteurs de plages et de lieux.
|
||||
*
|
||||
* LEFT JOIN volontaire : un modèle vidé de ses plages reste listé, avec un
|
||||
* compteur à zéro, plutôt que de disparaître silencieusement.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listForAgent(int $agentId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -26,6 +60,15 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrouve un modèle par (identifiant, agent).
|
||||
*
|
||||
* Le double critère est le contrôle de périmètre : il rend impossible la
|
||||
* lecture du modèle d'un autre agent en changeant l'identifiant dans l'URL.
|
||||
* Toutes les méthodes publiques passent par ici avant d'agir.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findForAgent(int $templateId, int $agentId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -38,6 +81,15 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plages d'un modèle, enrichies du lieu et du motif.
|
||||
*
|
||||
* `structure_actif` et `motif_actif` sont remontés parce qu'un lieu ou un
|
||||
* motif peut être désactivé APRÈS l'enregistrement du modèle : l'application
|
||||
* refuse alors le modèle, avec un message qui nomme l'élément fautif.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function entries(int $templateId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -60,6 +112,20 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une semaine réellement planifiée comme modèle réutilisable.
|
||||
*
|
||||
* Les dates de la semaine source sont converties en jours de la semaine :
|
||||
* c'est ce qui rend le modèle rejouable n'importe quand.
|
||||
*
|
||||
* @param bool $replaceExisting true pour écraser un modèle de même nom
|
||||
* (confirmation donnée par l'utilisateur après
|
||||
* un premier refus en 409).
|
||||
*
|
||||
* @return array{id_modele: int, nom: string, nombre_creneaux: int}
|
||||
*
|
||||
* @throws DomainException Nom invalide, semaine source vide, ou nom déjà pris.
|
||||
*/
|
||||
public function saveFromWeek(int $agentId, int $year, int $weekNumber, string $name, bool $replaceExisting = false): array
|
||||
{
|
||||
// La colonne `nom` fait 120 caractères : la borne annoncée à
|
||||
@@ -85,6 +151,11 @@ final class WeekTemplateModel extends BaseModel
|
||||
* @param array<int, mixed> $rawEntries
|
||||
* @param int[] $allowedMotifIds motifs affectables à cet agent
|
||||
* @param int[] $allowedStructureIds lieux actifs visibles par l'utilisateur
|
||||
*
|
||||
* @return array{id_modele: int, nom: string, nombre_creneaux: int}
|
||||
*
|
||||
* @throws DomainException Nom invalide, modèle vide, plage refusée,
|
||||
* chevauchement, ou nom déjà pris.
|
||||
*/
|
||||
public function saveFromEntries(
|
||||
int $agentId,
|
||||
@@ -104,6 +175,18 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $this->persistTemplate($agentId, $name, $entries, $replaceExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assainit et contrôle le nom d'un modèle.
|
||||
*
|
||||
* Les suites d'espaces sont réduites à un seul (motif /u, donc sûr en
|
||||
* UTF-8), afin que « Semaine type » et « Semaine type » ne coexistent pas
|
||||
* comme deux modèles distincts. La borne de 120 caractères est celle de la
|
||||
* colonne : contrôlée ici, elle produit un message clair au lieu d'une
|
||||
* troncature silencieuse de MariaDB. mb_strlen() compte des CARACTÈRES, pas
|
||||
* des octets.
|
||||
*
|
||||
* @throws DomainException Si le nom est vide ou trop long.
|
||||
*/
|
||||
private function normalizeName(string $name): string
|
||||
{
|
||||
$name = trim(preg_replace('/\s+/u', ' ', $name) ?? '');
|
||||
@@ -114,10 +197,24 @@ final class WeekTemplateModel extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les plages saisies dans l'éditeur et les met en forme.
|
||||
*
|
||||
* Chaque champ est revalidé côté serveur, y compris l'appartenance du motif
|
||||
* et du lieu aux listes autorisées calculées par le contrôleur. Les messages
|
||||
* d'erreur nomment le JOUR concerné : c'est le seul repère dont dispose
|
||||
* l'utilisateur pour retrouver la plage fautive dans la grille.
|
||||
*
|
||||
* Le tri final (jour, puis heure de début) rend l'ordre d'affichage stable
|
||||
* quel que soit l'ordre d'envoi du navigateur.
|
||||
*
|
||||
* @param array<int, mixed> $rawEntries
|
||||
* @param int[] $allowedMotifIds
|
||||
* @param int[] $allowedStructureIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*
|
||||
* @throws DomainException Plage illisible, jour hors lundi-vendredi,
|
||||
* horaires invalides, motif ou lieu non autorisé,
|
||||
* ou chevauchement.
|
||||
*/
|
||||
private function normalizeEditorEntries(array $rawEntries, array $allowedMotifIds, array $allowedStructureIds): array
|
||||
{
|
||||
@@ -182,6 +279,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrôle un horaire au quart d'heure et le ramène en HH:MM:SS.
|
||||
*
|
||||
* Accepte HH:MM comme HH:MM:SS en entrée (le navigateur envoie l'un ou
|
||||
* l'autre selon le champ) et normalise toujours vers HH:MM:SS.
|
||||
*
|
||||
* @param string $dayLabel Jour en toutes lettres, pour le message d'erreur.
|
||||
*
|
||||
* @throws DomainException Si l'horaire n'est pas un quart d'heure valide.
|
||||
*/
|
||||
private function normalizeQuarterHour(mixed $value, string $dayLabel): string
|
||||
{
|
||||
$time = trim((string) $value);
|
||||
@@ -198,8 +305,13 @@ final class WeekTemplateModel extends BaseModel
|
||||
* Deux plages du même jour ne peuvent pas se chevaucher, même sur deux
|
||||
* lieux différents : un agent n'est jamais à deux endroits à la fois.
|
||||
*
|
||||
* Comparaison en O(n²), assumée : un modèle plafonne à quelques dizaines de
|
||||
* plages.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entries
|
||||
* @param array<int, string> $jours
|
||||
* @param array<int, string> $jours Libellés indexés par numéro de jour.
|
||||
*
|
||||
* @throws DomainException Au premier chevauchement rencontré.
|
||||
*/
|
||||
private function assertEditorEntriesNoOverlap(array $entries, array $jours): void
|
||||
{
|
||||
@@ -227,7 +339,23 @@ final class WeekTemplateModel extends BaseModel
|
||||
* Écriture du modèle et de ses plages, quelle qu'en soit l'origine :
|
||||
* une semaine réellement planifiée ou l'éditeur de semaine type.
|
||||
*
|
||||
* L'unicité porte sur le couple (agent, nom). Le SELECT … FOR UPDATE ferme
|
||||
* la fenêtre entre le contrôle et l'insertion, où deux enregistrements
|
||||
* simultanés créeraient deux modèles homonymes.
|
||||
*
|
||||
* En remplacement, les plages sont SUPPRIMÉES puis réinsérées et l'en-tête
|
||||
* conserve son identifiant : les écrans qui référencent le modèle continuent
|
||||
* de le retrouver.
|
||||
*
|
||||
* Le message « Une semaine type porte déjà ce nom » est reconnu par
|
||||
* WeekTemplateController pour distinguer un 409 d'un 422 — le reformuler
|
||||
* casse cette distinction.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*
|
||||
* @return array{id_modele: int, nom: string, nombre_creneaux: int}
|
||||
*
|
||||
* @throws DomainException Si le nom est déjà pris et $replaceExisting est faux.
|
||||
*/
|
||||
private function persistTemplate(int $agentId, string $name, array $rows, bool $replaceExisting): array
|
||||
{
|
||||
@@ -290,6 +418,32 @@ final class WeekTemplateModel extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un modèle à UNE semaine précise.
|
||||
*
|
||||
* Les jours de la semaine du modèle sont projetés sur les dates réelles de
|
||||
* la semaine cible (jour 1 = lundi de cette semaine).
|
||||
*
|
||||
* Deux modes :
|
||||
* - `replace` : tous les créneaux de l'agent sur cette semaine sont
|
||||
* supprimés, TOUS LIEUX CONFONDUS — un modèle décrit une semaine
|
||||
* complète, pas le complément d'un lieu ;
|
||||
* - `merge` : les créneaux existants sont conservés et le moindre
|
||||
* chevauchement fait échouer l'opération entière.
|
||||
*
|
||||
* Le cache $planningByStructure évite de rechercher le planning à chaque
|
||||
* plage. Il est manipulé par référence (&$planningInfo) pour que la remise
|
||||
* en brouillon d'un planning soit visible aux itérations suivantes et ne
|
||||
* déclenche pas plusieurs UPDATE ; le `unset` en fin de boucle est
|
||||
* indispensable, une référence PHP survivant à l'itération.
|
||||
*
|
||||
* @param string $mode « merge » ou « replace ».
|
||||
*
|
||||
* @return array{template: array<string, mixed>, inserted: int, mode: string, reopened_planning_ids: int[]}
|
||||
*
|
||||
* @throws DomainException Mode invalide, modèle introuvable ou vide, lieu ou
|
||||
* motif désactivé depuis, ou conflit en mode merge.
|
||||
*/
|
||||
public function applyToWeek(int $templateId, int $agentId, int $year, int $weekNumber, string $mode): array
|
||||
{
|
||||
if (!in_array($mode, ['merge', 'replace'], true)) {
|
||||
@@ -450,6 +604,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simule l'application sur une période, SANS RIEN ÉCRIRE.
|
||||
*
|
||||
* Étape imposée par l'interface avant applyToPeriod() : l'opération touche
|
||||
* des dizaines de semaines et peut rouvrir des plannings validés.
|
||||
*
|
||||
* @param string $periodFilter all | school | extra.
|
||||
*
|
||||
* @return array<string, mixed> Voir buildPeriodPlan() pour le détail.
|
||||
*/
|
||||
public function previewPeriod(
|
||||
int $templateId,
|
||||
int $agentId,
|
||||
@@ -461,6 +625,26 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $this->buildPeriodPlan($templateId, $agentId, $startDate, $endDate, $mode, $periodFilter, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un modèle à toutes les semaines d'une période.
|
||||
*
|
||||
* Reprend le plan calculé par buildPeriodPlan(), puis écrit. Deux modes :
|
||||
* - `merge` : échec au moindre conflit, avec un message qui décrit le
|
||||
* PREMIER conflit rencontré et oriente vers l'aperçu ou le mode remplacement ;
|
||||
* - `replace` : les créneaux existants sont supprimés sur les SEULES DATES
|
||||
* RÉELLEMENT VISÉES ($targetDateLookup). Une date ignorée par le filtre
|
||||
* de calendrier n'est donc pas vidée — sans quoi appliquer un modèle
|
||||
* « périodes scolaires » effacerait aussi les vacances.
|
||||
*
|
||||
* Le tout dans UNE transaction : sur des centaines de créneaux, un échec à
|
||||
* mi-parcours laisserait un état ininterprétable.
|
||||
*
|
||||
* @return array<string, mixed> Compteurs détaillés (inserted, deleted,
|
||||
* weeks_touched, days_touched, skipped_*, reopened_planning_ids…),
|
||||
* repris tels quels dans le message rendu à l'utilisateur.
|
||||
*
|
||||
* @throws DomainException Aucune date éligible, ou conflit en mode merge.
|
||||
*/
|
||||
public function applyToPeriod(
|
||||
int $templateId,
|
||||
int $agentId,
|
||||
@@ -646,6 +830,14 @@ final class WeekTemplateModel extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un modèle et, par cascade SQL, ses plages.
|
||||
*
|
||||
* Les créneaux DÉJÀ APPLIQUÉS ne sont pas touchés : le modèle est un patron,
|
||||
* pas un lien vivant vers les plannings qu'il a produits.
|
||||
*
|
||||
* @return bool false si le modèle n'existe pas ou appartient à un autre agent.
|
||||
*/
|
||||
public function delete(int $templateId, int $agentId): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -655,6 +847,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->rowCount() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créneaux d'une semaine réelle, convertis en jours de la semaine.
|
||||
*
|
||||
* `WEEKDAY() + 1` donne 1 (lundi) à 5 (vendredi) — à ne pas confondre avec
|
||||
* DAYOFWEEK(), qui commence au dimanche. Le filtre `BETWEEN 0 AND 4` écarte
|
||||
* le week-end, et la contrainte sur les bornes de la semaine écarte un
|
||||
* créneau égaré hors de sa semaine.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function sourceWeekEntries(int $agentId, int $year, int $weekNumber): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -680,6 +882,39 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le plan d'application sur une période — cœur de previewPeriod() et
|
||||
* d'applyToPeriod(), qui partagent ainsi exactement le même calcul.
|
||||
*
|
||||
* Déroulé :
|
||||
* 1. contrôles d'entrée (mode, filtre, dates, plafond de deux ans — borne
|
||||
* de sécurité contre une saisie erronée qui produirait des dizaines de
|
||||
* milliers de créneaux) ;
|
||||
* 2. lecture du modèle, avec refus si un lieu ou un motif a été désactivé
|
||||
* depuis son enregistrement ;
|
||||
* 3. parcours JOUR PAR JOUR de la période, week-ends exclus. Chaque jour
|
||||
* est qualifié de PERISCOLAIRE ou EXTRASCOLAIRE selon qu'il tombe ou non
|
||||
* dans une période de vacances, puis retenu ou écarté selon
|
||||
* $periodFilter ;
|
||||
* 4. contrôle de cohérence interne du plan produit ;
|
||||
* 5. si $includeExistingAnalysis, confrontation aux créneaux déjà en base
|
||||
* pour dénombrer les conflits.
|
||||
*
|
||||
* Les plages écartées sont CONSERVÉES dans `skipped_entries` avec leur motif
|
||||
* d'exclusion : l'utilisateur doit pouvoir vérifier que le filtre a fait ce
|
||||
* qu'il attendait, un simple compteur ne le permettrait pas.
|
||||
*
|
||||
* @param bool $includeExistingAnalysis true en aperçu (on veut voir les
|
||||
* conflits sans verrouiller) ; false en application, où
|
||||
* applyToPeriod() refait l'analyse dans sa transaction, sous verrou.
|
||||
*
|
||||
* @return array<string, mixed> template, target_entries, skipped_entries,
|
||||
* conflicts, days, vacation_periods et summary.
|
||||
*
|
||||
* @throws DomainException Mode ou filtre invalide, dates incohérentes,
|
||||
* période trop longue, modèle introuvable ou vide,
|
||||
* lieu ou motif désactivé, chevauchement interne.
|
||||
*/
|
||||
private function buildPeriodPlan(
|
||||
int $templateId,
|
||||
int $agentId,
|
||||
@@ -861,6 +1096,17 @@ final class WeekTemplateModel extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Décode une date « AAAA-MM-JJ » en refusant tout report implicite.
|
||||
*
|
||||
* getLastErrors() est indispensable : sans ce contrôle, PHP accepterait
|
||||
* « 2026-02-31 » en le reportant au 3 mars. Le « ! » remet à zéro les champs
|
||||
* non fournis, sinon l'heure courante serait injectée.
|
||||
*
|
||||
* @param string $label Libellé du champ, repris dans le message d'erreur.
|
||||
*
|
||||
* @throws DomainException Si la date est invalide.
|
||||
*/
|
||||
private function parseStrictDate(string $value, string $label): DateTimeImmutable
|
||||
{
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
|
||||
@@ -872,6 +1118,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Période de vacances contenant cette date, ou null (jour scolaire).
|
||||
*
|
||||
* Comparaison de chaînes : le format ISO AAAA-MM-JJ se trie
|
||||
* chronologiquement, aucune conversion en objet n'est nécessaire.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $periods
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function vacationForDate(string $date, array $periods): ?array
|
||||
{
|
||||
foreach ($periods as $period) {
|
||||
@@ -882,6 +1138,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créneaux existants de l'agent sur une période, TOUS LIEUX confondus.
|
||||
*
|
||||
* @param bool $forUpdate true pour verrouiller les lignes ; à n'utiliser
|
||||
* que dans une transaction, et donc uniquement à l'application.
|
||||
* L'aperçu lit sans verrou, pour ne pas bloquer les autres
|
||||
* utilisateurs pendant une simple simulation.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function existingEntriesBetween(int $agentId, string $startDate, string $endDate, bool $forUpdate = false): array
|
||||
{
|
||||
$sql = "SELECT c.id_creneau, c.id_planning, p.statut, c.date_jour,
|
||||
@@ -907,6 +1173,17 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confronte le plan aux créneaux existants et liste TOUS les conflits.
|
||||
*
|
||||
* Contrairement aux assert*(), ne lève pas : l'aperçu doit présenter
|
||||
* l'ensemble des conflits, à charge de l'appelant de décider quoi en faire.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $targetEntries
|
||||
* @param array<int, array<string, mixed>> $existingEntries
|
||||
*
|
||||
* @return array<int, array<string, mixed>> Un élément par paire en conflit.
|
||||
*/
|
||||
private function findConflicts(array $targetEntries, array $existingEntries): array
|
||||
{
|
||||
$conflicts = [];
|
||||
@@ -932,6 +1209,15 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'identifiant de la semaine, en la créant si besoin.
|
||||
*
|
||||
* COPIE de PlanningModel::ensureWeek(), les deux modèles n'ayant pas de base
|
||||
* commune. Toute correction ici doit être reportée là-bas, et
|
||||
* réciproquement.
|
||||
*
|
||||
* @throws \RuntimeException Si la semaine n'a pu être ni créée ni retrouvée.
|
||||
*/
|
||||
private function ensureWeek(int $year, int $weekNumber, DateTimeImmutable $weekStart, DateTimeImmutable $weekEnd): int
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -959,6 +1245,18 @@ final class WeekTemplateModel extends BaseModel
|
||||
return $weekId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse un plan dont deux créneaux se chevauchent le même jour.
|
||||
*
|
||||
* Contrôle du plan PROJETÉ (dates réelles), là où
|
||||
* assertEditorEntriesNoOverlap() porte sur le modèle abstrait (jours de la
|
||||
* semaine). Les deux sont nécessaires : un modèle sain peut produire un plan
|
||||
* fautif si deux modèles sont appliqués sur la même période.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entries
|
||||
*
|
||||
* @throws DomainException Au premier chevauchement rencontré.
|
||||
*/
|
||||
private function assertTemplateNoOverlap(array $entries): void
|
||||
{
|
||||
for ($i = 0, $count = count($entries); $i < $count; $i++) {
|
||||
@@ -983,6 +1281,16 @@ final class WeekTemplateModel extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse un chevauchement avec les créneaux DÉJÀ en base sur la semaine.
|
||||
*
|
||||
* Utilisé par applyToWeek() en mode « merge ». La lecture couvre tous les
|
||||
* lieux de l'agent sur la semaine, et non le seul lieu visé.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entries
|
||||
*
|
||||
* @throws DomainException Au premier chevauchement rencontré.
|
||||
*/
|
||||
private function assertNoExistingOverlap(int $agentId, int $weekId, array $entries): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
|
||||
Reference in New Issue
Block a user