336 lines
13 KiB
PHP
336 lines
13 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\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 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' : ''),
|
||
]);
|
||
}
|
||
|
||
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')
|
||
),
|
||
]);
|
||
}
|
||
|
||
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,
|
||
]);
|
||
}
|
||
|
||
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,
|
||
]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|