pour prod

This commit is contained in:
Loic Masi
2026-08-07 16:13:43 +02:00
commit 5fbf76868f
157 changed files with 24085 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
<?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\Models\AgentModel;
use App\Models\QuotaModel;
use App\Models\WeekTemplateModel;
use DateTimeImmutable;
use DomainException;
use PDO;
use Throwable;
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);
}
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]);
}
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 denregistrer 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' : ''),
]);
}
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 dimport 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 dimporter 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')
),
]);
}
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 dapplication 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 lapplication 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,
]);
}
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 dapplication 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 dappliquer 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,
]);
}
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.']);
}
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;
}
}