554 lines
22 KiB
PHP
554 lines
22 KiB
PHP
<?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\Core\Security;
|
||
use App\Models\AgentModel;
|
||
use App\Models\MotifModel;
|
||
use App\Models\QuotaModel;
|
||
use App\Models\StructureModel;
|
||
use App\Models\WeekTemplateModel;
|
||
use DateTimeImmutable;
|
||
use DomainException;
|
||
use PDO;
|
||
use Throwable;
|
||
|
||
/**
|
||
* Semaines types : modèles lundi→vendredi rattachés à UN agent.
|
||
*
|
||
* Une semaine type n'est PAS un planning : elle décrit une organisation
|
||
* générique et ne produit aucun créneau tant qu'elle n'est pas appliquée à une
|
||
* semaine (apply) ou dupliquée sur une période (applyPeriod). Voir README_V7_10.md.
|
||
*
|
||
* Deux origines pour une semaine type, qui convergent vers
|
||
* WeekTemplateModel::persistTemplate() :
|
||
* - store() : capture d'une semaine réellement planifiée ;
|
||
* - storeEntries() : composition directe dans l'éditeur.
|
||
*
|
||
* Tout est réservé au Service Enfance : un modèle porte des créneaux sur
|
||
* plusieurs lieux, il déborde donc du périmètre d'un responsable de structure.
|
||
*
|
||
* Convention de codes d'erreur propre à ce contrôleur :
|
||
* - 409 / TEMPLATE_EXISTS : un modèle porte déjà ce nom (l'interface propose
|
||
* alors de le remplacer en renvoyant replace: true) ;
|
||
* - 422 / TEMPLATE_INVALID : contenu du modèle refusé.
|
||
* Le tri se fait sur le TEXTE du message d'exception (« déjà ce nom ») : toute
|
||
* reformulation de ce message dans WeekTemplateModel casse la distinction.
|
||
*/
|
||
final class WeekTemplateController
|
||
{
|
||
private WeekTemplateModel $templateModel;
|
||
private AgentModel $agentModel;
|
||
private QuotaModel $quotaModel;
|
||
private Access $access;
|
||
|
||
public function __construct(private PDO $pdo)
|
||
{
|
||
$this->templateModel = new WeekTemplateModel($pdo);
|
||
$this->agentModel = new AgentModel($pdo);
|
||
$this->quotaModel = new QuotaModel($pdo);
|
||
$this->access = new Access($pdo);
|
||
}
|
||
|
||
/**
|
||
* GET api/week_templates.php — liste des semaines types d'un agent.
|
||
*
|
||
* Ne renvoie que des compteurs, pas le contenu : la liste sert à choisir,
|
||
* show() sert à ouvrir.
|
||
*/
|
||
public function index(): void
|
||
{
|
||
$this->access->requireService();
|
||
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
|
||
if (!$agentId) {
|
||
JsonResponse::send(422, ['error' => 'Agent invalide.']);
|
||
}
|
||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
$templates = array_map(static function (array $template): array {
|
||
return [
|
||
'id' => (int) $template['id_modele'],
|
||
'name' => (string) $template['nom'],
|
||
'entry_count' => (int) $template['nombre_creneaux'],
|
||
'location_count' => (int) $template['nombre_lieux'],
|
||
'updated_at' => (string) $template['date_modification'],
|
||
];
|
||
}, $this->templateModel->listForAgent((int) $agentId));
|
||
|
||
JsonResponse::send(200, ['templates' => $templates]);
|
||
}
|
||
|
||
/**
|
||
* GET api/week_template.php — contenu détaillé d'une semaine type.
|
||
*
|
||
* Le modèle est retrouvé par (template_id, agent_id) et non par le seul
|
||
* identifiant : c'est ce qui empêche de lire le modèle d'un autre agent en
|
||
* changeant l'identifiant dans l'URL.
|
||
*
|
||
* Les drapeaux `structure_active` et `motif_actif` sont conservés bien que
|
||
* toujours vrais à la création : un lieu ou un motif peut être désactivé
|
||
* APRÈS l'enregistrement du modèle, et l'éditeur doit le signaler plutôt que
|
||
* d'échouer au moment de l'application.
|
||
*/
|
||
public function show(): void
|
||
{
|
||
$this->access->requireService();
|
||
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
|
||
$templateId = filter_input(INPUT_GET, 'template_id', FILTER_VALIDATE_INT);
|
||
|
||
if (!$agentId || !$templateId) {
|
||
JsonResponse::send(422, ['error' => 'Agent ou semaine type invalide.']);
|
||
}
|
||
|
||
$template = $this->templateModel->findForAgent((int) $templateId, (int) $agentId);
|
||
if ($template === null) {
|
||
JsonResponse::send(404, ['error' => 'Semaine type introuvable pour cet agent.']);
|
||
}
|
||
|
||
$entries = array_map(static function (array $entry): array {
|
||
return [
|
||
'jour_semaine' => (int) $entry['jour_semaine'],
|
||
'id_structure' => (int) $entry['id_structure'],
|
||
'id_motif' => (int) $entry['id_motif'],
|
||
// L'éditeur manipule des heures au format HH:MM.
|
||
'heure_debut' => substr((string) $entry['heure_debut'], 0, 5),
|
||
'heure_fin' => substr((string) $entry['heure_fin'], 0, 5),
|
||
'structure_nom' => (string) $entry['structure_nom'],
|
||
'structure_active' => (int) $entry['structure_actif'] === 1,
|
||
'motif_libelle' => (string) $entry['motif_libelle'],
|
||
'motif_actif' => (int) $entry['motif_actif'] === 1,
|
||
'compte_dans_quota' => (int) $entry['compte_dans_quota'] === 1,
|
||
];
|
||
}, $this->templateModel->entries((int) $templateId));
|
||
|
||
JsonResponse::send(200, [
|
||
'template' => [
|
||
'id' => (int) $template['id_modele'],
|
||
'name' => (string) $template['nom'],
|
||
'updated_at' => (string) $template['date_modification'],
|
||
],
|
||
'entries' => $entries,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/save_week_template_entries.php — enregistre un modèle composé
|
||
* directement dans l'éditeur (V7.10).
|
||
*
|
||
* Les listes de motifs et de lieux autorisés sont calculées ICI puis
|
||
* transmises au modèle : celui-ci ne connaît pas les règles d'accès et se
|
||
* contente de rejeter ce qui n'est pas dans les listes reçues.
|
||
*
|
||
* Charge utile : agent_id, name, entries[], replace (bool), csrf_token.
|
||
*/
|
||
public function storeEntries(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$name = (string) ($payload['name'] ?? '');
|
||
$entries = $payload['entries'] ?? null;
|
||
$replace = filter_var($payload['replace'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||
|
||
if (!$agentId || !is_array($entries)) {
|
||
JsonResponse::send(422, ['error' => 'Agent ou contenu de la semaine type invalide.']);
|
||
}
|
||
|
||
$agent = $this->agentModel->findActive((int) $agentId);
|
||
if ($agent === null) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
// Mêmes règles qu'à la saisie : « Formation » n'est pas affectable à un
|
||
// agent sans droit à l'enveloppe, et un lieu inactif n'est pas choisissable.
|
||
$allowedMotifIds = (new MotifModel($this->pdo))->activeIdsForAgent($agent);
|
||
$allowedStructureIds = array_map(
|
||
static fn (array $structure): int => (int) $structure['id_structure'],
|
||
(new StructureModel($this->pdo))->allActive()
|
||
);
|
||
|
||
try {
|
||
$template = $this->templateModel->saveFromEntries(
|
||
(int) $agentId,
|
||
$name,
|
||
$entries,
|
||
$allowedMotifIds,
|
||
$allowedStructureIds,
|
||
$replace
|
||
);
|
||
} catch (DomainException $e) {
|
||
$status = str_contains($e->getMessage(), 'déjà ce nom') ? 409 : 422;
|
||
JsonResponse::send($status, [
|
||
'error' => $e->getMessage(),
|
||
'code' => $status === 409 ? 'TEMPLATE_EXISTS' : 'TEMPLATE_INVALID',
|
||
]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible d’enregistrer la semaine type.',
|
||
'details' => Security::debugEnabled() ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'template' => [
|
||
'id' => (int) $template['id_modele'],
|
||
'name' => (string) $template['nom'],
|
||
'entry_count' => (int) $template['nombre_creneaux'],
|
||
],
|
||
'message' => sprintf(
|
||
'La semaine type « %s » a été enregistrée avec %d plage%s.',
|
||
$template['nom'],
|
||
$template['nombre_creneaux'],
|
||
$template['nombre_creneaux'] > 1 ? 's' : ''
|
||
),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/save_week_template.php — capture une semaine planifiée en modèle.
|
||
*
|
||
* Point de départ historique : l'utilisateur saisit une semaine « normale »,
|
||
* puis la fige comme modèle réutilisable.
|
||
*
|
||
* Charge utile : agent_id, week, name, replace (bool), csrf_token.
|
||
*/
|
||
public function store(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$week = $this->parseWeek(trim((string) ($payload['week'] ?? '')));
|
||
$name = trim((string) ($payload['name'] ?? ''));
|
||
$replace = filter_var($payload['replace'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||
|
||
if (!$agentId || $week === null) {
|
||
JsonResponse::send(422, ['error' => 'Agent ou semaine invalide.']);
|
||
}
|
||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
try {
|
||
[$year, $weekNumber] = $week;
|
||
$template = $this->templateModel->saveFromWeek((int) $agentId, $year, $weekNumber, $name, $replace);
|
||
} catch (DomainException $e) {
|
||
$status = str_contains($e->getMessage(), 'déjà ce nom') ? 409 : 422;
|
||
JsonResponse::send($status, [
|
||
'error' => $e->getMessage(),
|
||
'code' => $status === 409 ? 'TEMPLATE_EXISTS' : 'TEMPLATE_INVALID',
|
||
]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible d’enregistrer la semaine type.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'template' => $template,
|
||
'message' => sprintf('La semaine type « %s » a été enregistrée avec %d créneau%s.', $template['nom'], $template['nombre_creneaux'], $template['nombre_creneaux'] > 1 ? 'x' : ''),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/apply_week_template.php — applique un modèle à UNE semaine.
|
||
*
|
||
* Deux modes : `merge` complète les créneaux existants, `replace` les
|
||
* remplace. Appliquer sur une semaine validée la repasse en brouillon
|
||
* (`requires_revalidation`).
|
||
*
|
||
* Charge utile : template_id, agent_id, week, mode (merge|replace), csrf_token.
|
||
*/
|
||
public function apply(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$templateId = filter_var($payload['template_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$week = $this->parseWeek(trim((string) ($payload['week'] ?? '')));
|
||
$mode = strtolower(trim((string) ($payload['mode'] ?? 'merge')));
|
||
|
||
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) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
try {
|
||
[$year, $weekNumber] = $week;
|
||
$result = $this->templateModel->applyToWeek((int) $templateId, (int) $agentId, $year, $weekNumber, $mode);
|
||
} catch (DomainException $e) {
|
||
JsonResponse::send(409, ['error' => $e->getMessage()]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible d’importer la semaine type.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
|
||
$reopened = count($result['reopened_planning_ids']) > 0;
|
||
$message = sprintf(
|
||
'La semaine type « %s » a été appliquée : %d créneau%s importé%s.',
|
||
$result['template']['nom'],
|
||
$result['inserted'],
|
||
$result['inserted'] > 1 ? 'x' : '',
|
||
$result['inserted'] > 1 ? 's' : ''
|
||
);
|
||
if ($reopened) {
|
||
$message .= ' Les plannings validés modifiés ont été repassés en brouillon.';
|
||
}
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'message' => $message,
|
||
'requires_revalidation' => $reopened,
|
||
'quota' => $this->quotaModel->forDateForApi(
|
||
(int) $agentId,
|
||
(new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->format('Y-m-d')
|
||
),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/preview_week_template_period.php — simulation SANS écriture.
|
||
*
|
||
* Étape obligatoire dans l'interface avant applyPeriod() : une application
|
||
* sur période touche des dizaines de semaines et peut rouvrir des plannings
|
||
* validés. L'utilisateur doit voir le volume avant de confirmer.
|
||
*
|
||
* `period_filter` restreint le périmètre au calendrier scolaire :
|
||
* all → toutes les semaines de l'intervalle ;
|
||
* school → semaines scolaires seulement (périscolaire) ;
|
||
* extra → vacances scolaires seulement (extrascolaire).
|
||
*
|
||
* Charge utile : template_id, agent_id, start_date, end_date, mode,
|
||
* period_filter, csrf_token.
|
||
*/
|
||
public function previewPeriod(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$templateId = filter_var($payload['template_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$startDate = trim((string) ($payload['start_date'] ?? ''));
|
||
$endDate = trim((string) ($payload['end_date'] ?? ''));
|
||
$mode = strtolower(trim((string) ($payload['mode'] ?? 'merge')));
|
||
$periodFilter = strtolower(trim((string) ($payload['period_filter'] ?? 'all')));
|
||
|
||
if (
|
||
!$templateId
|
||
|| !$agentId
|
||
|| $startDate === ''
|
||
|| $endDate === ''
|
||
|| !in_array($mode, ['merge', 'replace'], true)
|
||
|| !in_array($periodFilter, ['all', 'school', 'extra'], true)
|
||
) {
|
||
JsonResponse::send(422, ['error' => 'Semaine type, agent, période, filtre de calendrier ou mode d’application invalide.']);
|
||
}
|
||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
try {
|
||
$preview = $this->templateModel->previewPeriod(
|
||
(int) $templateId,
|
||
(int) $agentId,
|
||
$startDate,
|
||
$endDate,
|
||
$mode,
|
||
$periodFilter
|
||
);
|
||
} catch (DomainException $e) {
|
||
JsonResponse::send(422, ['error' => $e->getMessage()]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible de prévisualiser l’application de la semaine type.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
|
||
// Les créneaux détaillés sont utiles au modèle mais pas nécessaires au navigateur.
|
||
unset($preview['target_entries']);
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'preview' => $preview,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/apply_week_template_period.php — applique un modèle sur une période.
|
||
*
|
||
* Mêmes paramètres que previewPeriod(), mais avec écriture. Le message de
|
||
* retour est volontairement détaillé (créés / ignorés / remplacés / rouverts) :
|
||
* l'opération est massive, difficile à annuler à la main, et l'utilisateur
|
||
* doit pouvoir vérifier a posteriori qu'elle a fait ce qu'il attendait.
|
||
*
|
||
* Les quotas sont renvoyés indexés par date de début de période : une
|
||
* application à cheval sur deux années civiles touche DEUX quotas annuels
|
||
* distincts, et l'interface doit rafraîchir les deux.
|
||
*/
|
||
public function applyPeriod(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$templateId = filter_var($payload['template_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$startDate = trim((string) ($payload['start_date'] ?? ''));
|
||
$endDate = trim((string) ($payload['end_date'] ?? ''));
|
||
$mode = strtolower(trim((string) ($payload['mode'] ?? 'merge')));
|
||
$periodFilter = strtolower(trim((string) ($payload['period_filter'] ?? 'all')));
|
||
|
||
if (
|
||
!$templateId
|
||
|| !$agentId
|
||
|| $startDate === ''
|
||
|| $endDate === ''
|
||
|| !in_array($mode, ['merge', 'replace'], true)
|
||
|| !in_array($periodFilter, ['all', 'school', 'extra'], true)
|
||
) {
|
||
JsonResponse::send(422, ['error' => 'Semaine type, agent, période, filtre de calendrier ou mode d’application invalide.']);
|
||
}
|
||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||
}
|
||
|
||
try {
|
||
$result = $this->templateModel->applyToPeriod(
|
||
(int) $templateId,
|
||
(int) $agentId,
|
||
$startDate,
|
||
$endDate,
|
||
$mode,
|
||
$periodFilter
|
||
);
|
||
} catch (DomainException $e) {
|
||
JsonResponse::send(409, ['error' => $e->getMessage()]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible d’appliquer la semaine type sur la période.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
|
||
$message = sprintf(
|
||
'La semaine type « %s » a été appliquée du %s au %s : %d créneau%s créé%s sur %d jour%s et %d semaine%s.',
|
||
$result['template']['name'],
|
||
$result['start_date'],
|
||
$result['end_date'],
|
||
$result['inserted'],
|
||
$result['inserted'] > 1 ? 'x' : '',
|
||
$result['inserted'] > 1 ? 's' : '',
|
||
$result['days_touched'],
|
||
$result['days_touched'] > 1 ? 's' : '',
|
||
$result['weeks_touched'],
|
||
$result['weeks_touched'] > 1 ? 's' : ''
|
||
);
|
||
|
||
$scopeLabel = match ($result['period_filter']) {
|
||
'school' => 'uniquement sur les périodes scolaires',
|
||
'extra' => 'uniquement sur les périodes extrascolaires',
|
||
default => 'sur toutes les semaines de la période',
|
||
};
|
||
$message .= ' Périmètre : ' . $scopeLabel . '.';
|
||
|
||
if ($result['skipped_count'] > 0) {
|
||
$message .= sprintf(
|
||
' %d créneau%s ignoré%s pour respecter les périodes scolaires/vacances.',
|
||
$result['skipped_count'],
|
||
$result['skipped_count'] > 1 ? 'x' : '',
|
||
$result['skipped_count'] > 1 ? 's' : ''
|
||
);
|
||
}
|
||
if ($result['deleted'] > 0) {
|
||
$message .= sprintf(' %d ancien%s créneau%s remplacé%s.', $result['deleted'], $result['deleted'] > 1 ? 's' : '', $result['deleted'] > 1 ? 'x' : '', $result['deleted'] > 1 ? 's' : '');
|
||
}
|
||
if ($result['reopened_planning_ids'] !== []) {
|
||
$message .= ' Les plannings validés modifiés ont été repassés en brouillon.';
|
||
}
|
||
|
||
$quotas = [];
|
||
foreach (array_unique([(string) $result['start_date'], (string) $result['end_date']]) as $contextDate) {
|
||
$quota = $this->quotaModel->forDateForApi((int) $agentId, $contextDate);
|
||
$quotas[(string) $quota['date_debut_periode']] = $quota;
|
||
}
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'message' => $message,
|
||
'result' => $result,
|
||
'requires_revalidation' => $result['reopened_planning_ids'] !== [],
|
||
'quotas' => $quotas,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST api/delete_week_template.php — supprime une semaine type.
|
||
*
|
||
* La suppression est filtrée par agent_id ; un modèle appartenant à un autre
|
||
* agent ressort en 404 plutôt qu'en 403, pour ne pas révéler son existence.
|
||
* Les créneaux DÉJÀ appliqués ne sont pas touchés : le modèle n'est qu'un
|
||
* patron, pas un lien vivant vers les plannings produits.
|
||
*/
|
||
public function destroy(): void
|
||
{
|
||
$this->access->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$templateId = filter_var($payload['template_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
if (!$templateId || !$agentId) {
|
||
JsonResponse::send(422, ['error' => 'Semaine type ou agent invalide.']);
|
||
}
|
||
|
||
if (!$this->templateModel->delete((int) $templateId, (int) $agentId)) {
|
||
JsonResponse::send(404, ['error' => 'Semaine type introuvable.']);
|
||
}
|
||
|
||
JsonResponse::send(200, ['success' => true, 'message' => 'Semaine type supprimée.']);
|
||
}
|
||
|
||
/**
|
||
* Décode « AAAA-Www ». Copie locale, comme dans les autres contrôleurs.
|
||
*
|
||
* @return array{0: int, 1: int}|null [année ISO, semaine], ou null.
|
||
*/
|
||
private function parseWeek(string $value): ?array
|
||
{
|
||
if (!preg_match('/^(\d{4})-W(\d{2})$/', $value, $matches)) {
|
||
return null;
|
||
}
|
||
$year = (int) $matches[1];
|
||
$week = (int) $matches[2];
|
||
return $week >= 1 && $week <= 53 ? [$year, $week] : null;
|
||
}
|
||
}
|