ptaModel = new PtaModel($pdo); $this->agentModel = new AgentModel($pdo); $this->structureModel = new StructureModel($pdo); $this->access = new Access($pdo); } /** * GET api/pta_summary.php — synthèse du PTA d'un agent à une date donnée. * * La date sert de point d'observation : elle détermine la période de quota * retenue et les contraintes en vigueur ce jour-là. Elle retombe sur * aujourd'hui si elle n'est pas fournie. * * Paramètres GET : agent_id, date (facultative, « AAAA-MM-JJ »). */ public function summary(): void { $this->access->requireService(); $agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT); $date = trim((string) ($_GET['date'] ?? date('Y-m-d'))); $parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $date); if (!$agentId || !$parsed || $parsed->format('Y-m-d') !== $date) { JsonResponse::send(422, ['error' => 'Agent ou date de référence invalide.']); } try { JsonResponse::send(200, $this->ptaModel->summary((int) $agentId, $date)); } catch (Throwable $e) { JsonResponse::send(500, [ 'error' => 'Impossible de calculer le PTA de l’agent.', 'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null, ]); } } /** * POST api/pta_constraint.php — enregistre ou supprime une contrainte. * * Deux types seulement, volontairement fermés : MEDICALE et * TEMPS_PARTIEL_THERAPEUTIQUE. Une date de fin vide signifie « sans terme * connu », cas courant d'un aménagement médical en cours. * * Le commentaire est OBLIGATOIRE : une contrainte restreint la planification * d'un agent, elle doit rester justifiable a posteriori. * * Les maximums arrivent en HEURES depuis le formulaire et sont convertis en * MINUTES pour la base, unité de tous les cumuls de l'application. * * Charge utile : action (SAVE|DELETE), agent_id, csrf_token, puis selon * l'action constraint_id, ou type_contrainte / date_debut / date_fin / * commentaire / quotite_temporaire / maximum_heures_jour / * maximum_heures_semaine / interdit_matin|midi|soir. */ public function constraint(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $action = strtoupper(trim((string) ($payload['action'] ?? 'SAVE'))); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT); if (!$agentId || $this->agentModel->findActive((int) $agentId) === null) { JsonResponse::send(422, ['error' => 'Agent invalide.']); } if ($action === 'DELETE') { $constraintId = filter_var($payload['constraint_id'] ?? null, FILTER_VALIDATE_INT); if (!$constraintId) { JsonResponse::send(422, ['error' => 'Contrainte invalide.']); } $deleted = $this->ptaModel->deleteConstraint((int) $constraintId, (int) $agentId); if ($deleted) { Journal::ecriture( 'CONTRAINTE_SUPPRIMEE', 'agent_contrainte', (int) $constraintId, sprintf('Contrainte prioritaire #%d supprimée pour l’agent #%d.', (int) $constraintId, (int) $agentId), ['id_agent' => (int) $agentId] ); } JsonResponse::send($deleted ? 200 : 404, [ 'message' => $deleted ? 'La contrainte a été supprimée.' : 'Contrainte introuvable.', ]); } $type = strtoupper(trim((string) ($payload['type_contrainte'] ?? ''))); $start = trim((string) ($payload['date_debut'] ?? '')); $end = trim((string) ($payload['date_fin'] ?? '')); $comment = trim((string) ($payload['commentaire'] ?? '')); $startDate = DateTimeImmutable::createFromFormat('!Y-m-d', $start); $endDate = $end !== '' ? DateTimeImmutable::createFromFormat('!Y-m-d', $end) : null; if (!in_array($type, ['MEDICALE', 'TEMPS_PARTIEL_THERAPEUTIQUE'], true) || !$startDate || $startDate->format('Y-m-d') !== $start || ($end !== '' && (!$endDate || $endDate->format('Y-m-d') !== $end || $endDate < $startDate)) || $comment === '') { JsonResponse::send(422, ['error' => 'Type, dates ou commentaire de contrainte invalides.']); } $rate = $payload['quotite_temporaire'] ?? null; $rate = ($rate === null || $rate === '') ? null : filter_var($rate, FILTER_VALIDATE_FLOAT); if ($rate !== null && ($rate === false || $rate <= 0 || $rate > 100)) { JsonResponse::send(422, ['error' => 'La quotité temporaire doit être comprise entre 1 et 100 %.']); } $maxDayHours = $payload['maximum_heures_jour'] ?? null; $maxWeekHours = $payload['maximum_heures_semaine'] ?? null; $maxDayMinutes = ($maxDayHours === null || $maxDayHours === '') ? null : (int) round((float) $maxDayHours * 60); $maxWeekMinutes = ($maxWeekHours === null || $maxWeekHours === '') ? null : (int) round((float) $maxWeekHours * 60); try { $id = $this->ptaModel->saveConstraint([ 'id_agent' => (int) $agentId, 'type_contrainte' => $type, 'date_debut' => $start, 'date_fin' => $end !== '' ? $end : null, 'quotite_temporaire' => $rate, 'maximum_minutes_jour' => $maxDayMinutes, 'maximum_minutes_semaine' => $maxWeekMinutes, 'interdit_matin' => !empty($payload['interdit_matin']) ? 1 : 0, 'interdit_midi' => !empty($payload['interdit_midi']) ? 1 : 0, 'interdit_soir' => !empty($payload['interdit_soir']) ? 1 : 0, 'commentaire' => $comment, ]); Journal::ecriture( 'CONTRAINTE_ENREGISTREE', 'agent_contrainte', (int) $id, sprintf('Contrainte « %s » enregistrée pour l’agent #%d à partir du %s.', $type, (int) $agentId, $start), ['id_agent' => (int) $agentId, 'type_contrainte' => $type, 'date_debut' => $start, 'date_fin' => $end] ); JsonResponse::send(201, ['message' => 'La contrainte prioritaire a été enregistrée.', 'id_contrainte' => $id]); } catch (Throwable $e) { // INCOHÉRENCE CONNUE : le détail de l'exception est renvoyé au // navigateur SANS AUCUNE condition de debug, contrairement au reste // du projet. Idem dans wish(). À aligner sur // Security::debugEnabled() — voir la note de PlanningController::save(). JsonResponse::send(500, ['error' => 'Impossible d’enregistrer la contrainte.', 'details' => $e->getMessage()]); } } /** * POST api/pta_wish.php — enregistre ou supprime un souhait de lieu. * * Deux types : PREFERENCE (l'agent souhaite y être affecté) et INTERDICTION * (il ne doit pas l'être). La priorité, de 1 à 5, permet d'ordonner * plusieurs préférences entre elles ; la distance en kilomètres sert à * arbitrer les affectations éloignées. * * Charge utile : action (SAVE|DELETE), agent_id, csrf_token, puis selon * l'action wish_id, ou structure_id / type_souhait / priorite / * distance_km / commentaire. */ public function wish(): void { $this->access->requireService(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $action = strtoupper(trim((string) ($payload['action'] ?? 'SAVE'))); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT); if (!$agentId || $this->agentModel->findActive((int) $agentId) === null) { JsonResponse::send(422, ['error' => 'Agent invalide.']); } if ($action === 'DELETE') { $wishId = filter_var($payload['wish_id'] ?? null, FILTER_VALIDATE_INT); if (!$wishId) { JsonResponse::send(422, ['error' => 'Préférence invalide.']); } $deleted = $this->ptaModel->deleteWish((int) $wishId, (int) $agentId); if ($deleted) { Journal::ecriture( 'SOUHAIT_SUPPRIME', 'agent_structure_souhait', (int) $wishId, sprintf('Préférence de lieu #%d supprimée pour l’agent #%d.', (int) $wishId, (int) $agentId), ['id_agent' => (int) $agentId] ); } JsonResponse::send($deleted ? 200 : 404, [ 'message' => $deleted ? 'La préférence ou interdiction a été supprimée.' : 'Élément introuvable.', ]); } $structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT); $type = strtoupper(trim((string) ($payload['type_souhait'] ?? ''))); $priority = filter_var($payload['priorite'] ?? 1, FILTER_VALIDATE_INT); $distance = $payload['distance_km'] ?? null; $distance = ($distance === null || $distance === '') ? null : filter_var($distance, FILTER_VALIDATE_FLOAT); $comment = trim((string) ($payload['commentaire'] ?? '')); if (!$structureId || $this->structureModel->findActive((int) $structureId) === null || !in_array($type, ['PREFERENCE', 'INTERDICTION'], true) || !$priority || $priority < 1 || $priority > 5 || ($distance !== null && ($distance === false || $distance < 0))) { JsonResponse::send(422, ['error' => 'Lieu, type, priorité ou distance invalide.']); } try { $id = $this->ptaModel->saveWish([ 'id_agent' => (int) $agentId, 'id_structure' => (int) $structureId, 'type_souhait' => $type, 'priorite' => (int) $priority, 'distance_km' => $distance, 'commentaire' => $comment !== '' ? $comment : null, ]); Journal::ecriture( 'SOUHAIT_ENREGISTRE', 'agent_structure_souhait', (int) $id, sprintf('Préférence « %s » enregistrée pour l’agent #%d sur le lieu #%d.', $type, (int) $agentId, (int) $structureId), ['id_agent' => (int) $agentId, 'id_structure' => (int) $structureId, 'type_souhait' => $type] ); JsonResponse::send(201, ['message' => 'La préférence de lieu a été enregistrée.', 'id_souhait' => $id]); } catch (Throwable $e) { JsonResponse::send(500, ['error' => 'Impossible d’enregistrer la préférence.', 'details' => $e->getMessage()]); } } }