agentModel = new AgentModel($pdo); $this->structureModel = new StructureModel($pdo); $this->quotaModel = new QuotaModel($pdo); $this->access = new Access($pdo); } public function listForPlanning(): void { $this->access->requireDraftEditor(); $selectedLocation = filter_input(INPUT_GET, 'structure_id', FILTER_VALIDATE_INT) ?: 0; if ($selectedLocation) { $this->access->requireStructureEdit((int) $selectedLocation); } JsonResponse::send(200, ['agents' => $this->agentModel->listForLocation((int) $selectedLocation)]); } public function overview(): void { $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $weekValue = trim((string) ($_GET['week'] ?? '')); $week = $this->parseWeek($weekValue); if (!$agentId || $week === null) { JsonResponse::send(400, ['error' => 'Agent ou semaine invalide.']); } $this->access->requireAgentView((int) $agentId); $agent = $this->agentModel->findActive((int) $agentId); if ($agent === null) { JsonResponse::send(404, ['error' => 'Agent introuvable.']); } [$year, $weekNumber] = $week; $weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->format('Y-m-d'); $weekEnd = (new DateTimeImmutable())->setISODate($year, $weekNumber, 7)->format('Y-m-d'); JsonResponse::send(200, [ 'agent' => $agent, 'week' => ['value' => $weekValue, 'date_debut' => $weekStart, 'date_fin' => $weekEnd], 'quota' => $this->quotaModel->forDateForApi((int) $agentId, $weekStart), 'entries' => $this->agentModel->entriesForWeek((int) $agentId, $year, $weekNumber), ]); } public function monthOverview(): void { $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $monthValue = trim((string) ($_GET['month'] ?? '')); if (!$agentId || !preg_match('/^(\d{4})-(\d{2})$/', $monthValue, $matches)) { JsonResponse::send(400, ['error' => 'Agent ou mois invalide.']); } $this->access->requireAgentView((int) $agentId); $year = (int) $matches[1]; $monthNumber = (int) $matches[2]; if ($monthNumber < 1 || $monthNumber > 12) { JsonResponse::send(400, ['error' => 'Mois invalide.']); } $agent = $this->agentModel->findActive((int) $agentId); if ($agent === null) { JsonResponse::send(404, ['error' => 'Agent introuvable.']); } $firstDay = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $monthNumber)); $lastDay = $firstDay->modify('last day of this month'); $gridStart = $firstDay->modify('-' . ((int) $firstDay->format('N') - 1) . ' days'); $lastWeekMonday = $lastDay->modify('-' . ((int) $lastDay->format('N') - 1) . ' days'); $gridEnd = $lastWeekMonday->modify('+4 days'); $dayLabels = [1 => 'Lundi', 2 => 'Mardi', 3 => 'Mercredi', 4 => 'Jeudi', 5 => 'Vendredi']; $monthLabels = [ 1 => 'Janvier', 2 => 'Février', 3 => 'Mars', 4 => 'Avril', 5 => 'Mai', 6 => 'Juin', 7 => 'Juillet', 8 => 'Août', 9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Décembre', ]; $weeks = []; for ($monday = $gridStart; $monday <= $lastWeekMonday; $monday = $monday->modify('+7 days')) { $days = []; for ($weekday = 1; $weekday <= 5; $weekday++) { $date = $monday->modify('+' . ($weekday - 1) . ' days'); $days[] = [ 'date' => $date->format('Y-m-d'), 'label' => $dayLabels[$weekday], 'display' => $date->format('d/m'), 'in_month' => (int) $date->format('n') === $monthNumber, ]; } $weeks[] = [ 'value' => sprintf('%04d-W%02d', (int) $monday->format('o'), (int) $monday->format('W')), 'number' => (int) $monday->format('W'), 'date_debut' => $monday->format('Y-m-d'), 'date_fin' => $monday->modify('+4 days')->format('Y-m-d'), 'days' => $days, ]; } JsonResponse::send(200, [ 'agent' => $agent, 'month' => [ 'value' => $monthValue, 'year' => $year, 'month' => $monthNumber, 'label' => $monthLabels[$monthNumber] . ' ' . $year, 'date_debut' => $firstDay->format('Y-m-d'), 'date_fin' => $lastDay->format('Y-m-d'), 'grid_start' => $gridStart->format('Y-m-d'), 'grid_end' => $gridEnd->format('Y-m-d'), ], 'weeks' => $weeks, 'quota' => $this->quotaModel->forDateForApi((int) $agentId, $firstDay->format('Y-m-d')), 'entries' => $this->agentModel->entriesForDateRange( (int) $agentId, $gridStart->format('Y-m-d'), $gridEnd->format('Y-m-d') ), ]); } public function timeSummaryPdf(): void { $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $dateValue = trim((string) ($_GET['date'] ?? '')); $year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT); if ($dateValue === '' && $year) { $dateValue = sprintf('%04d-01-01', (int) $year); } $date = DateTimeImmutable::createFromFormat('!Y-m-d', $dateValue); if (!$agentId || !$date || $date->format('Y-m-d') !== $dateValue) { http_response_code(400); echo 'Agent ou date de référence invalide.'; return; } $this->access->requireAgentView((int) $agentId); $agent = $this->agentModel->findActive((int) $agentId); if ($agent === null) { http_response_code(404); echo 'Agent introuvable.'; return; } $quota = $this->quotaModel->forDateForApi((int) $agentId, $dateValue); $summary = $this->agentModel->timeSummaryForRange( (int) $agentId, (string) $quota['date_debut_periode'], (string) $quota['date_fin_periode'] ); $pdf = (new AgentTimeSummaryPdfService())->annualSummary($agent, $quota, $summary); PdfResponse::inline( $pdf, sprintf( 'bilan_heures_%s_%s_%s_%s.pdf', $agent['nom'], $agent['prenom'], str_replace('-', '', (string) $quota['date_debut_periode']), str_replace('-', '', (string) $quota['date_fin_periode']) ) ); } public function pdf(): void { $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $weekValue = trim((string) ($_GET['week'] ?? '')); $week = $this->parseWeek($weekValue); $weekCount = filter_input(INPUT_GET, 'weeks', FILTER_VALIDATE_INT) ?: 7; $weekCount = in_array((int) $weekCount, [6, 7], true) ? (int) $weekCount : 7; if (!$agentId || $week === null) { http_response_code(400); echo 'Agent ou semaine invalide.'; return; } $this->access->requireAgentView((int) $agentId); $agent = $this->agentModel->findActive((int) $agentId); if ($agent === null) { http_response_code(404); echo 'Agent introuvable.'; return; } [$year, $weekNumber] = $week; $firstMonday = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1); $weeks = []; for ($index = 0; $index < $weekCount; $index++) { $monday = $firstMonday->modify('+' . ($index * 7) . ' days'); $isoYear = (int) $monday->format('o'); $isoWeek = (int) $monday->format('W'); $weeks[] = [ 'value' => sprintf('%04d-W%02d', $isoYear, $isoWeek), 'date_debut' => $monday->format('Y-m-d'), 'date_fin' => $monday->modify('+6 days')->format('Y-m-d'), ]; } $lastFriday = $firstMonday->modify('+' . (($weekCount - 1) * 7 + 4) . ' days'); $entries = $this->agentModel->entriesForDateRange( (int) $agentId, $firstMonday->format('Y-m-d'), $lastFriday->format('Y-m-d') ); $pdf = (new PlanningPdfService())->agentWeeks( $agent, $entries, $weeks, $this->quotaModel->forDateForApi((int) $agentId, $firstMonday->format('Y-m-d')) ); PdfResponse::inline( $pdf, sprintf( 'planning_%s_%s_%s_%d_semaines.pdf', $agent['nom'], $agent['prenom'], str_replace('-W', '_S', $weekValue), $weekCount ) ); } public function hoursContractPdf(): void { $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $dateValue = trim((string) ($_GET['date'] ?? '')); $year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT); if ($dateValue === '' && $year) { $dateValue = sprintf('%04d-01-01', (int) $year); } $date = DateTimeImmutable::createFromFormat('!Y-m-d', $dateValue); if (!$agentId || !$date || $date->format('Y-m-d') !== $dateValue) { http_response_code(400); echo 'Agent ou date de référence invalide.'; return; } $this->access->requireAgentView((int) $agentId); $agent = $this->agentModel->findActive((int) $agentId); if ($agent === null) { http_response_code(404); echo 'Agent introuvable.'; return; } $quota = $this->quotaModel->forDateForApi((int) $agentId, $dateValue); $pdf = (new AgentHoursContractPdfService())->annualContract($agent, $quota); PdfResponse::inline( $pdf, sprintf( 'contrat_horaire_%s_%s_%s.pdf', $agent['nom'], $agent['prenom'], str_replace('-', '', (string) $quota['date_debut_periode']) ) ); } public function assignments(): void { $this->access->requireService(); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') { JsonResponse::send(200, ['agents' => $this->agentModel->allActive()]); } Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT); $rawStructure = $payload['structure_id'] ?? null; $structureId = ($rawStructure === null || $rawStructure === '' || $rawStructure === 0) ? null : filter_var($rawStructure, FILTER_VALIDATE_INT); if (!$agentId) { JsonResponse::send(422, ['error' => 'Agent invalide.']); } if ($this->agentModel->findActive((int) $agentId) === null) { JsonResponse::send(404, ['error' => 'Agent introuvable.']); } if ($structureId !== null && $this->structureModel->findActive((int) $structureId) === null) { JsonResponse::send(422, ['error' => 'Lieu d’affectation invalide ou inactif.']); } try { $this->agentModel->updateAssignment((int) $agentId, $structureId !== null ? (int) $structureId : null); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de modifier les paramètres de l’agent.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } JsonResponse::send(200, [ 'message' => $structureId === null ? 'Les paramètres de l’agent ont été mis à jour sans lieu principal.' : 'Le lieu principal de l’agent a été mis à jour.', ]); } public function create(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $matricule = trim((string) ($payload['matricule'] ?? '')); $nom = trim((string) ($payload['nom'] ?? '')); $prenom = trim((string) ($payload['prenom'] ?? '')); $email = trim((string) ($payload['email'] ?? '')); $telephone = trim((string) ($payload['telephone'] ?? '')); $adresse = trim((string) ($payload['adresse'] ?? '')); $estDiplome = filter_var($payload['est_diplome'] ?? false, FILTER_VALIDATE_BOOLEAN); $diplomeLibelle = trim((string) ($payload['diplome_libelle'] ?? '')); $structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT); $rate = filter_var($payload['quotite_travail'] ?? null, FILTER_VALIDATE_FLOAT); $contractStart = trim((string) ($payload['date_debut_contrat'] ?? '')); $contractStartDate = DateTimeImmutable::createFromFormat('!Y-m-d', $contractStart); $contractEnd = trim((string) ($payload['date_fin_contrat'] ?? '')); $contractEndDate = $contractEnd !== '' ? DateTimeImmutable::createFromFormat('!Y-m-d', $contractEnd) : null; $postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT); $contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT'))); $trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN); $ptaComment = trim((string) ($payload['commentaire_pta'] ?? '')); $validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts()); if ($matricule === '' || $nom === '' || $prenom === '' || !$structureId || !$postId || !in_array((int) $postId, $validPostIds, true) || !in_array($contractType, ['PERMANENT', 'TEMPORAIRE'], true) || !$contractStartDate || $contractStartDate->format('Y-m-d') !== $contractStart || ($contractEnd !== '' && (!$contractEndDate || $contractEndDate->format('Y-m-d') !== $contractEnd || $contractEndDate < $contractStartDate))) { JsonResponse::send(422, ['error' => 'Le matricule, le nom, le prénom, le poste, le type de contrat, le lieu principal et des dates de contrat cohérentes sont obligatoires.']); } if ($rate === false || $rate <= 0 || $rate > 100) { JsonResponse::send(422, ['error' => 'La quotité de travail doit être comprise entre 1 et 100 %.']); } if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) { JsonResponse::send(422, ['error' => 'L’adresse e-mail saisie n’est pas valide.']); } if (strlen($matricule) > 50 || strlen($nom) > 100 || strlen($prenom) > 100 || strlen($email) > 255 || strlen($telephone) > 30 || strlen($adresse) > 255 || strlen($diplomeLibelle) > 150 || strlen($ptaComment) > 1000) { JsonResponse::send(422, ['error' => 'Un des champs dépasse la longueur autorisée.']); } if ($this->structureModel->findActive((int) $structureId) === null) { JsonResponse::send(422, ['error' => 'Le lieu d’affectation principal sélectionné est invalide ou inactif.']); } if ($this->agentModel->matriculeExists($matricule)) { JsonResponse::send(409, ['error' => 'Un agent utilise déjà ce matricule.']); } try { $created = $this->agentModel->createWithDefaults([ 'matricule' => $matricule, 'nom' => $nom, 'prenom' => $prenom, 'email' => $email !== '' ? $email : null, 'telephone' => $telephone !== '' ? $telephone : null, 'adresse' => $adresse !== '' ? $adresse : null, 'est_diplome' => $estDiplome, 'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null, 'structure_id' => (int) $structureId, 'quotite_travail' => (float) $rate, 'date_debut_contrat' => $contractStart, 'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null, 'id_poste' => (int) $postId, 'type_contrat' => $contractType, 'formation_repartition_annuelle' => $trainingPlanned, 'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null, ]); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de créer l’agent.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } $structure = $this->structureModel->findActive((int) $structureId); JsonResponse::send(201, [ 'message' => sprintf('%s %s a été ajouté avec le lieu principal « %s ».', $prenom, $nom, $structure['nom']), 'agent' => array_merge($created, [ 'matricule' => $matricule, 'nom' => $nom, 'prenom' => $prenom, 'email' => $email !== '' ? $email : null, 'telephone' => $telephone !== '' ? $telephone : null, 'adresse' => $adresse !== '' ? $adresse : null, 'est_diplome' => $estDiplome ? 1 : 0, 'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null, 'id_structure' => (int) $structureId, 'structure_nom' => (string) $structure['nom'], 'structure_type_affectation' => (string) $structure['type_affectation'], 'quotite_travail' => (float) $rate, 'date_debut_contrat' => $contractStart, 'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null, 'id_poste' => (int) $postId, 'poste_libelle' => (string) (array_values(array_filter($this->agentModel->allPosts(), static fn(array $post): bool => (int) $post['id_poste'] === (int) $postId))[0]['libelle'] ?? ''), 'type_contrat' => $contractType, 'formation_repartition_annuelle' => $trainingPlanned ? 1 : 0, 'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null, ]), ]); } public function updateProfile(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT); $email = trim((string) ($payload['email'] ?? '')); $telephone = trim((string) ($payload['telephone'] ?? '')); $adresse = trim((string) ($payload['adresse'] ?? '')); $estDiplome = filter_var($payload['est_diplome'] ?? false, FILTER_VALIDATE_BOOLEAN); $diplomeLibelle = trim((string) ($payload['diplome_libelle'] ?? '')); $contractStart = trim((string) ($payload['date_debut_contrat'] ?? '')); $contractStartDate = DateTimeImmutable::createFromFormat('!Y-m-d', $contractStart); $contractEnd = trim((string) ($payload['date_fin_contrat'] ?? '')); $contractEndDate = $contractEnd !== '' ? DateTimeImmutable::createFromFormat('!Y-m-d', $contractEnd) : null; $postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT); $contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT'))); $trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN); $ptaComment = trim((string) ($payload['commentaire_pta'] ?? '')); $validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts()); $rawStructure = $payload['structure_id'] ?? null; $structureId = ($rawStructure === null || $rawStructure === '' || $rawStructure === 0) ? null : filter_var($rawStructure, FILTER_VALIDATE_INT); if (!$agentId) { JsonResponse::send(422, ['error' => 'Agent invalide.']); } if ($this->agentModel->findActive((int) $agentId) === null) { JsonResponse::send(404, ['error' => 'Agent introuvable.']); } if (!$postId || !in_array((int) $postId, $validPostIds, true) || !in_array($contractType, ['PERMANENT', 'TEMPORAIRE'], true) || !$contractStartDate || $contractStartDate->format('Y-m-d') !== $contractStart || ($contractEnd !== '' && (!$contractEndDate || $contractEndDate->format('Y-m-d') !== $contractEnd || $contractEndDate < $contractStartDate))) { JsonResponse::send(422, ['error' => 'Le poste, le type de contrat et les dates du contrat doivent être valides.']); } if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) { JsonResponse::send(422, ['error' => 'L’adresse e-mail saisie n’est pas valide.']); } if (strlen($email) > 255 || strlen($telephone) > 30 || strlen($adresse) > 255 || strlen($diplomeLibelle) > 150 || strlen($ptaComment) > 1000) { JsonResponse::send(422, ['error' => 'Un des champs dépasse la longueur autorisée.']); } if ($structureId !== null && $this->structureModel->findActive((int) $structureId) === null) { JsonResponse::send(422, ['error' => 'Lieu d’affectation principal invalide ou inactif.']); } try { $this->agentModel->updateProfile((int) $agentId, [ 'email' => $email !== '' ? $email : null, 'telephone' => $telephone !== '' ? $telephone : null, 'adresse' => $adresse !== '' ? $adresse : null, 'est_diplome' => $estDiplome, 'diplome_libelle' => $estDiplome && $diplomeLibelle !== '' ? $diplomeLibelle : null, 'date_debut_contrat' => $contractStart, 'date_fin_contrat' => $contractEnd !== '' ? $contractEnd : null, 'id_poste' => (int) $postId, 'type_contrat' => $contractType, 'formation_repartition_annuelle' => $trainingPlanned, 'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null, 'structure_id' => $structureId !== null ? (int) $structureId : null, ]); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de modifier les informations de l’agent.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } JsonResponse::send(200, [ 'message' => 'Les informations de l’agent ont été mises à jour.', 'agent' => $this->agentModel->findActive((int) $agentId), ]); } private function parseWeek(string $value): ?array { if (!preg_match('/^(\d{4})-W(\d{2})$/', $value, $matches)) { return null; } $week = (int) $matches[2]; return $week >= 1 && $week <= 53 ? [(int) $matches[1], $week] : null; } }