vacationModel = new VacationModel($pdo); $this->access = new Access($pdo); } public function index(): void { $this->access->requireService(); $schoolYear = trim((string) ($_GET['school_year'] ?? '')); if ($schoolYear !== '' && !$this->isSchoolYear($schoolYear)) { JsonResponse::send(422, ['error' => 'Année scolaire invalide. Format attendu : 2026-2027.']); } JsonResponse::send(200, [ 'periods' => $this->vacationModel->all($schoolYear !== '' ? $schoolYear : null), ]); } public function preview(): void { $this->access->requireService(); $academy = trim((string) ($_GET['academy'] ?? '')); $calendarYearRaw = trim((string) ($_GET['year'] ?? '')); if ($academy === '' || (function_exists('mb_strlen') ? mb_strlen($academy, 'UTF-8') : strlen($academy)) > 100) { JsonResponse::send(422, ['error' => 'Renseignez une académie valide.']); } if (!preg_match('/^\d{4}$/', $calendarYearRaw)) { JsonResponse::send(422, ['error' => 'Année civile invalide. Format attendu : 2026.']); } $calendarYear = (int) $calendarYearRaw; if ($calendarYear < 2000 || $calendarYear > 2100) { JsonResponse::send(422, ['error' => 'Année civile invalide.']); } try { $result = (new SchoolHolidayApiService())->previewCalendarYearDetailed($academy, $calendarYear); $periods = $result['periods']; $warnings = $result['warnings']; } catch (Throwable $e) { JsonResponse::send(502, [ 'error' => 'Impossible de charger les propositions depuis l’API officielle.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } JsonResponse::send(200, [ 'academy' => $academy, 'year' => $calendarYear, 'periods' => $periods, 'warnings' => $warnings ?? [], 'message' => $periods === [] ? 'Aucune période n’a été trouvée pour l’année ' . $calendarYear . '.' : sprintf('%d période%s proposée%s pour toute l’année %d. Aucune donnée n’a été enregistrée en base.', count($periods), count($periods) > 1 ? 's' : '', count($periods) > 1 ? 's' : '', $calendarYear), ]); } public function save(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $rawPeriods = $payload['periods'] ?? []; if (!is_array($rawPeriods) || $rawPeriods === []) { JsonResponse::send(422, ['error' => 'Sélectionnez au moins une période à enregistrer.']); } if (count($rawPeriods) > 50) { JsonResponse::send(422, ['error' => 'Trop de périodes ont été envoyées en une seule fois.']); } $periods = []; foreach ($rawPeriods as $index => $raw) { if (!is_array($raw)) { JsonResponse::send(422, ['error' => 'Période invalide à la ligne ' . ($index + 1) . '.']); } $label = trim((string) ($raw['libelle'] ?? '')); $start = trim((string) ($raw['date_debut'] ?? '')); $end = trim((string) ($raw['date_fin'] ?? '')); $schoolYear = trim((string) ($raw['annee_scolaire'] ?? '')); $academy = trim((string) ($raw['academie'] ?? '')); $zone = trim((string) ($raw['zone'] ?? '')); $source = strtoupper(trim((string) ($raw['source'] ?? 'MANUEL'))); if ($label === '' || (function_exists('mb_strlen') ? mb_strlen($label, 'UTF-8') : strlen($label)) > 150 || !$this->isDate($start) || !$this->isDate($end) || $end < $start) { JsonResponse::send(422, ['error' => 'Dates ou libellé invalides à la ligne ' . ($index + 1) . '.']); } if (!$this->isSchoolYear($schoolYear)) { JsonResponse::send(422, ['error' => 'Année scolaire invalide à la ligne ' . ($index + 1) . '.']); } if (!in_array($source, ['MANUEL', 'DATA_GOUV'], true)) { $source = 'MANUEL'; } $periods[] = [ 'libelle' => $label, 'date_debut' => $start, 'date_fin' => $end, 'annee_scolaire' => $schoolYear, 'academie' => function_exists('mb_substr') ? mb_substr($academy, 0, 100, 'UTF-8') : substr($academy, 0, 100), 'zone' => function_exists('mb_substr') ? mb_substr($zone, 0, 50, 'UTF-8') : substr($zone, 0, 50), 'source' => $source, ]; } $count = $this->vacationModel->saveMany($periods); JsonResponse::send(200, [ 'success' => true, 'count' => $count, 'message' => sprintf('%d période%s de vacances enregistrée%s après validation manuelle.', $count, $count > 1 ? 's' : '', $count > 1 ? 's' : ''), ]); } public function delete(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $id = filter_var($payload['period_id'] ?? null, FILTER_VALIDATE_INT); if (!$id) { JsonResponse::send(422, ['error' => 'Période invalide.']); } if (!$this->vacationModel->delete((int) $id)) { JsonResponse::send(404, ['error' => 'Période introuvable.']); } JsonResponse::send(200, ['success' => true, 'message' => 'Période supprimée.']); } private function isDate(string $value): bool { $date = DateTimeImmutable::createFromFormat('!Y-m-d', $value); return $date !== false && $date->format('Y-m-d') === $value; } private function isSchoolYear(string $value): bool { if (!preg_match('/^(\d{4})-(\d{4})$/', $value, $matches)) { return false; } return (int) $matches[2] === (int) $matches[1] + 1; } }