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; } }