This commit is contained in:
PTA
2026-08-20 17:00:06 +02:00
parent 5d8c0993a6
commit 94e7f09eb4
58 changed files with 5605 additions and 615 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Controllers;
use App\Core\Access;
use App\Core\Csrf;
use App\Core\Journal;
use App\Core\JsonResponse;
use App\Core\Request;
use App\Core\Security;
@@ -200,6 +201,14 @@ final class WeekTemplateController
]);
}
Journal::ecriture(
'SEMAINE_TYPE_ENREGISTREE',
'modele_semaine_agent',
(int) $template['id_modele'],
sprintf('Semaine type « %s » enregistrée depuis l’éditeur pour l’agent #%d.', (string) $template['nom'], (int) $agentId),
['id_agent' => (int) $agentId, 'nombre_creneaux' => (int) $template['nombre_creneaux'], 'remplacement' => $replace]
);
JsonResponse::send(200, [
'success' => true,
'template' => [
@@ -259,6 +268,14 @@ final class WeekTemplateController
]);
}
Journal::ecriture(
'SEMAINE_TYPE_ENREGISTREE',
'modele_semaine_agent',
isset($template['id_modele']) ? (int) $template['id_modele'] : null,
sprintf('Semaine type « %s » enregistrée depuis une semaine planifiée pour l’agent #%d.', $name, (int) $agentId),
['id_agent' => (int) $agentId, 'nombre_creneaux' => (int) $template['nombre_creneaux'], 'remplacement' => $replace]
);
JsonResponse::send(200, [
'success' => true,
'template' => $template,
@@ -290,12 +307,18 @@ final class WeekTemplateController
if (!$templateId || !$agentId || $week === null || !in_array($mode, ['merge', 'replace'], true)) {
JsonResponse::send(422, ['error' => 'Semaine type, agent, semaine ou mode d’import invalide.']);
}
if ($this->agentModel->findActive((int) $agentId) === null) {
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
}
[$year, $weekNumber] = $week;
$lundi = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1);
$this->assertDatesWithinContract($agent, array_map(
static fn(int $offset): string => $lundi->modify('+' . $offset . ' day')->format('Y-m-d'),
range(0, 4)
));
try {
[$year, $weekNumber] = $week;
$result = $this->templateModel->applyToWeek((int) $templateId, (int) $agentId, $year, $weekNumber, $mode);
} catch (DomainException $e) {
JsonResponse::send(409, ['error' => $e->getMessage()]);
@@ -318,6 +341,27 @@ final class WeekTemplateController
$message .= ' Les plannings validés modifiés ont été repassés en brouillon.';
}
Journal::ecriture(
'SEMAINE_TYPE_APPLIQUEE',
'planning',
null,
sprintf(
'Semaine type « %s » appliquée à l’agent #%d sur la semaine %d de %d : %d créneau(x).',
(string) $result['template']['nom'],
(int) $agentId,
$weekNumber,
$year,
(int) $result['inserted']
),
[
'id_agent' => (int) $agentId,
'id_modele' => (int) $templateId,
'annee' => $year,
'semaine' => $weekNumber,
'mode' => $mode,
]
);
JsonResponse::send(200, [
'success' => true,
'message' => $message,
@@ -368,9 +412,11 @@ final class WeekTemplateController
) {
JsonResponse::send(422, ['error' => 'Semaine type, agent, période, filtre de calendrier ou mode d’application invalide.']);
}
if ($this->agentModel->findActive((int) $agentId) === null) {
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
}
$this->assertPeriodWithinContract($agent, $startDate, $endDate);
try {
$preview = $this->templateModel->previewPeriod(
@@ -435,9 +481,11 @@ final class WeekTemplateController
) {
JsonResponse::send(422, ['error' => 'Semaine type, agent, période, filtre de calendrier ou mode d’application invalide.']);
}
if ($this->agentModel->findActive((int) $agentId) === null) {
$agent = $this->agentModel->findActive((int) $agentId);
if ($agent === null) {
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
}
$this->assertPeriodWithinContract($agent, $startDate, $endDate);
try {
$result = $this->templateModel->applyToPeriod(
@@ -499,6 +547,27 @@ final class WeekTemplateController
$quotas[(string) $quota['date_debut_periode']] = $quota;
}
Journal::ecriture(
'SEMAINE_TYPE_APPLIQUEE_PERIODE',
'planning',
null,
sprintf(
'Semaine type #%d appliquée à l’agent #%d du %s au %s.',
(int) $templateId,
(int) $agentId,
$startDate,
$endDate
),
[
'id_agent' => (int) $agentId,
'id_modele' => (int) $templateId,
'date_debut' => $startDate,
'date_fin' => $endDate,
'mode' => $mode,
'filtre_periode' => $periodFilter,
]
);
JsonResponse::send(200, [
'success' => true,
'message' => $message,
@@ -533,6 +602,14 @@ final class WeekTemplateController
JsonResponse::send(404, ['error' => 'Semaine type introuvable.']);
}
Journal::ecriture(
'SEMAINE_TYPE_SUPPRIMEE',
'modele_semaine_agent',
(int) $templateId,
sprintf('Semaine type #%d supprimée pour l’agent #%d.', (int) $templateId, (int) $agentId),
['id_agent' => (int) $agentId]
);
JsonResponse::send(200, ['success' => true, 'message' => 'Semaine type supprimée.']);
}
@@ -541,6 +618,51 @@ final class WeekTemplateController
*
* @return array{0: int, 1: int}|null [année ISO, semaine], ou null.
*/
/**
* Refuse toute affectation hors des dates de contrat de l'agent (V7.15).
*
* Copie assumée de PlanningController::assertDatesWithinContract() : les deux
* contrôleurs n'ont pas de base commune, et la règle doit s'appliquer des
* deux côtés. Le MESSAGE, lui, est construit une seule fois par AgentModel —
* c'est lui qui doit rester unique, pas la garde.
*
* @param array<string, mixed> $agent
* @param array<int, string> $dates
*/
private function assertDatesWithinContract(array $agent, array $dates): void
{
$hors = AgentModel::datesOutsideContract($agent, $dates);
if ($hors === []) {
return;
}
JsonResponse::send(422, [
'error' => AgentModel::contractViolationMessage($agent, $hors),
'code' => 'HORS_CONTRAT',
'dates' => $hors,
]);
}
/**
* Même règle, pour une PÉRIODE entière.
*
* Seules les bornes sont contrôlées, et c'est suffisant : le contrat est un
* intervalle continu, donc une période dont les deux extrémités y tombent y
* est entièrement contenue. Contrôler chacun des jours d'une duplication
* annuelle coûterait 365 comparaisons pour la même réponse.
*
* Le refus porte volontairement sur la période DEMANDÉE et non sur les seuls
* jours réellement écrits : personne ne doit avoir à deviner qu'une
* duplication « du 1er janvier au 31 décembre » n'a rien produit en novembre
* parce que le contrat s'arrêtait en octobre.
*
* @param array<string, mixed> $agent
*/
private function assertPeriodWithinContract(array $agent, string $startDate, string $endDate): void
{
$this->assertDatesWithinContract($agent, [$startDate, $endDate]);
}
private function parseWeek(string $value): ?array
{
if (!preg_match('/^(\d{4})-W(\d{2})$/', $value, $matches)) {