pdo))->requireService(); $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT); $year ??= (int) date('Y'); if (!$agentId || $year < 2000 || $year > 2100) { JsonResponse::send(422, ['error' => 'Agent ou année invalide.']); } try { JsonResponse::send(200, (new AnnualOverviewModel($this->pdo))->overview((int) $agentId, (int) $year)); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de générer la vue annuelle.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } } /** * Contrôle puis validation en une seule opération de tous les plannings * en brouillon de l'agent pour l'année civile affichée. * * Aucune validation partielle : si une erreur est trouvée, aucun planning * n'est validé. Les semaines concernées sont retournées au front afin de * pouvoir les ouvrir dans un nouvel onglet et les corriger. */ public function validateYear(): void { (new Access($this->pdo))->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT); $year = filter_var($payload['year'] ?? null, FILTER_VALIDATE_INT); $forceQuota = filter_var($payload['force_quota'] ?? false, FILTER_VALIDATE_BOOLEAN); if (!$agentId || !$year || $year < 2000 || $year > 2100) { JsonResponse::send(422, ['error' => 'Agent ou année invalide.']); } try { $planningModel = new PlanningModel($this->pdo); $quotaModel = new QuotaModel($this->pdo); $plannings = $planningModel->pendingValidationForAgentYear((int) $agentId, (int) $year); if ($plannings === []) { JsonResponse::send(200, [ 'success' => true, 'status' => 'nothing_to_validate', 'message' => 'Aucun planning en brouillon avec des créneaux n’est à valider pour cette année.', 'validated_count' => 0, 'plannings' => [], 'quota' => $quotaModel->annualForApi((int) $agentId, (int) $year), ]); } $results = []; $hasErrors = false; foreach ($plannings as $planning) { $controls = $this->controlsForPlanning($planningModel, $planning); $errors = array_values(array_filter( $controls, static fn(array $row): bool => $row['niveau'] === 'ERREUR' )); $hasErrors = $hasErrors || $errors !== []; $results[] = [ 'id_planning' => (int) $planning['id_planning'], 'id_structure' => (int) $planning['id_structure'], 'structure_nom' => (string) $planning['structure_nom'], 'annee' => (int) $planning['annee'], 'numero_semaine' => (int) $planning['numero_semaine'], 'date_debut' => (string) $planning['date_debut'], 'date_fin' => (string) $planning['date_fin'], 'nombre_creneaux' => (int) $planning['nombre_creneaux'], 'total_minutes' => (int) $planning['total_minutes'], 'controls' => $controls, 'has_errors' => $errors !== [], ]; } $quota = $quotaModel->annualForApi((int) $agentId, (int) $year); $quotaWarning = (int) $quota['minutes_restantes'] < 0; if ($hasErrors) { JsonResponse::send(422, [ 'success' => false, 'status' => 'errors', 'error' => 'Certaines semaines contiennent des erreurs. Aucune semaine n’a été validée.', 'plannings' => $results, 'quota' => $quota, ]); } if ($quotaWarning && !$forceQuota) { JsonResponse::send(409, [ 'success' => false, 'status' => 'warning', 'message' => sprintf( 'Le quota annuel est dépassé de %s. Confirmez pour valider malgré cet avertissement.', $quotaModel->formatMinutes(abs((int) $quota['minutes_restantes']))['libelle'] ), 'plannings' => $results, 'quota' => $quota, ]); } $ids = array_map(static fn(array $planning): int => (int) $planning['id_planning'], $plannings); $validatedCount = $planningModel->markValidatedMany($ids); JsonResponse::send(200, [ 'success' => true, 'status' => 'validated', 'validated_count' => $validatedCount, 'message' => sprintf( '%d planning(s) ont été validés pour l’année %d.', $validatedCount, (int) $year ), 'plannings' => $results, 'quota' => $quotaModel->annualForApi((int) $agentId, (int) $year), ]); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de contrôler ou valider l’année complète.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } } private function controlsForPlanning(PlanningModel $model, array $planning): array { $planningId = (int) $planning['id_planning']; $controls = []; if ($model->countEntries($planningId) === 0) { $this->addControl($controls, 'ERREUR', 'AUCUN_CRENEAU', 'Le planning ne contient aucune affectation.'); } foreach ($model->entriesOutsideWeek($planningId, (string) $planning['date_debut'], (string) $planning['date_fin']) as $row) { $this->addControl( $controls, 'ERREUR', 'CRENEAU_HORS_SEMAINE', sprintf('L’affectation #%d du %s est en dehors de la semaine sélectionnée.', $row['id_creneau'], $row['date_jour']) ); } foreach ($model->internalOverlaps($planningId) as $row) { $this->addControl( $controls, 'ERREUR', 'CHEVAUCHEMENT_INTERNE', sprintf('Les affectations #%d et #%d se chevauchent le %s.', $row['c1'], $row['c2'], $row['date_jour']) ); } foreach ($model->externalOverlaps($planningId) as $row) { $this->addControl( $controls, 'ERREUR', 'CHEVAUCHEMENT_AUTRE_STRUCTURE', sprintf( 'L’affectation #%d chevauche l’affectation #%d sur le lieu « %s » le %s (%s).', $row['c1'], $row['c2'], $row['structure_nom'], $row['date_jour'], strtolower((string) $row['autre_statut']) ) ); } if ($controls === []) { $this->addControl($controls, 'INFO', 'OK', 'Aucune anomalie bloquante détectée pour cette semaine.'); } return $controls; } private function addControl(array &$controls, string $level, string $code, string $message): void { $controls[] = ['niveau' => $level, 'code' => $code, 'message' => $message]; } }