Files
pta/app/Controllers/WeekTemplateController.php
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

336 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}