pdo->query( self::AGENT_SELECT . " WHERE a.actif = TRUE ORDER BY a.nom, a.prenom" )->fetchAll(); } public function allVisibleForStructure(int $structureId): array { $stmt = $this->pdo->prepare( self::AGENT_SELECT . " WHERE a.actif = TRUE AND ( a.id_structure = :structure_id_default OR EXISTS ( SELECT 1 FROM planning p WHERE p.id_agent = a.id_agent AND p.id_structure = :structure_id_planning ) ) ORDER BY a.nom, a.prenom" ); $stmt->execute([ 'structure_id_default' => $structureId, 'structure_id_planning' => $structureId, ]); return $stmt->fetchAll(); } public function allEligibleForPta(): array { return $this->pdo->query( self::AGENT_SELECT . " WHERE a.actif = TRUE AND a.type_contrat = 'PERMANENT' AND a.id_poste IS NOT NULL ORDER BY a.nom, a.prenom" )->fetchAll(); } public function allPosts(): array { return $this->pdo->query( "SELECT id_poste, code, libelle, famille, heures_mercredi_minutes, heures_extrascolaire_minutes, autorise_preparation, peut_assurer_animation, preparation_lundi_si_100 FROM poste_agent WHERE actif = TRUE ORDER BY ordre_affichage, libelle" )->fetchAll(); } public function listForLocation(int $selectedLocation = 0): array { $stmt = $this->pdo->prepare( "SELECT a.id_agent, a.matricule, a.nom, a.prenom, a.id_structure, a.id_poste, po.libelle AS poste_libelle, s.nom AS structure_nom, s.type_affectation AS structure_type_affectation, CASE WHEN a.id_structure = :selected_location THEN 1 ELSE 0 END AS lieu_principal_selectionne FROM agent a LEFT JOIN poste_agent po ON po.id_poste = a.id_poste LEFT JOIN structure s ON s.id_structure = a.id_structure WHERE a.actif = TRUE ORDER BY lieu_principal_selectionne DESC, a.nom, a.prenom" ); $stmt->execute(['selected_location' => $selectedLocation]); return $stmt->fetchAll(); } public function findActive(int $agentId): ?array { $stmt = $this->pdo->prepare( self::AGENT_SELECT . " WHERE a.id_agent = :id AND a.actif = TRUE LIMIT 1" ); $stmt->execute(['id' => $agentId]); return $stmt->fetch() ?: null; } public function matriculeExists(string $matricule): bool { $stmt = $this->pdo->prepare('SELECT 1 FROM agent WHERE matricule = :matricule LIMIT 1'); $stmt->execute(['matricule' => $matricule]); return $stmt->fetchColumn() !== false; } public function createWithDefaults(array $data): array { $contractStart = new DateTimeImmutable((string) $data['date_debut_contrat']); $contractEnd = !empty($data['date_fin_contrat']) ? new DateTimeImmutable((string) $data['date_fin_contrat']) : null; $year = (int) $contractStart->format('Y'); // Le quota PTA est suivi sur l'année civile, indépendamment de la date // anniversaire du contrat. La date de contrat reste une donnée RH. $periodStart = new DateTimeImmutable(sprintf('%04d-01-01', $year)); $periodEnd = new DateTimeImmutable(sprintf('%04d-12-31', $year)); $quotaReference = QuotaModel::REFERENCE_MINUTES; $quotaTarget = (int) round($quotaReference * ((float) $data['quotite_travail'] / 100)); $trainingEnvelope = !empty($data['formation_repartition_annuelle']) ? 0 : 840; try { $this->pdo->beginTransaction(); $insertAgent = $this->pdo->prepare( 'INSERT INTO agent ( matricule, nom, prenom, email, telephone, adresse, est_diplome, diplome_libelle, date_debut_contrat, id_poste, type_contrat, date_fin_contrat, formation_repartition_annuelle, commentaire_pta, id_structure, actif ) VALUES ( :matricule, :nom, :prenom, :email, :telephone, :adresse, :est_diplome, :diplome_libelle, :date_debut_contrat, :id_poste, :type_contrat, :date_fin_contrat, :formation_repartition_annuelle, :commentaire_pta, :structure_id, TRUE )' ); $insertAgent->execute([ 'matricule' => $data['matricule'], 'nom' => $data['nom'], 'prenom' => $data['prenom'], 'email' => $data['email'], 'telephone' => $data['telephone'], 'adresse' => $data['adresse'], 'est_diplome' => $data['est_diplome'] ? 1 : 0, 'diplome_libelle' => $data['est_diplome'] ? $data['diplome_libelle'] : null, 'date_debut_contrat' => $contractStart->format('Y-m-d'), 'id_poste' => $data['id_poste'], 'type_contrat' => $data['type_contrat'], 'date_fin_contrat' => $contractEnd?->format('Y-m-d'), 'formation_repartition_annuelle' => $data['formation_repartition_annuelle'] ? 1 : 0, 'commentaire_pta' => $data['commentaire_pta'], 'structure_id' => $data['structure_id'], ]); $agentId = (int) $this->pdo->lastInsertId(); $history = $this->pdo->prepare( 'INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif) VALUES (:agent_id, :structure_id, :date_debut, NULL, TRUE)' ); $history->execute([ 'agent_id' => $agentId, 'structure_id' => $data['structure_id'], 'date_debut' => $contractStart->format('Y-m-d'), ]); $quota = $this->pdo->prepare( 'INSERT INTO quota_agent_annuel ( id_agent, annee, quotite_travail, quota_reference_minutes, quota_cible_minutes, commentaire ) VALUES ( :agent_id, :annee, :quotite, :quota_reference, :quota_cible, :commentaire )' ); $quota->execute([ 'agent_id' => $agentId, 'annee' => $year, 'quotite' => $data['quotite_travail'], 'quota_reference' => $quotaReference, 'quota_cible' => $quotaTarget, 'commentaire' => 'Quota PTA de référence - année civile', ]); $pta = $this->pdo->prepare( 'INSERT INTO pta_annuel ( id_agent, date_debut, date_fin, quotite_travail, quota_reference_minutes, quota_cible_minutes, enveloppe_formation_minutes, statut ) VALUES ( :agent_id, :date_debut, :date_fin, :quotite, :quota_reference, :quota_cible, :formation, \'BROUILLON\' )' ); $pta->execute([ 'agent_id' => $agentId, 'date_debut' => $periodStart->format('Y-m-d'), 'date_fin' => $periodEnd->format('Y-m-d'), 'quotite' => $data['quotite_travail'], 'quota_reference' => $quotaReference, 'quota_cible' => $quotaTarget, 'formation' => $trainingEnvelope, ]); $this->pdo->commit(); return [ 'id_agent' => $agentId, 'annee_quota' => $year, 'date_debut_contrat' => $contractStart->format('Y-m-d'), 'date_fin_contrat' => $contractEnd?->format('Y-m-d'), 'quota_cible_minutes' => $quotaTarget, 'id_pta' => (int) $this->pdo->lastInsertId(), ]; } catch (Throwable $e) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $e; } } public function updateProfile(int $agentId, array $data): void { try { $this->pdo->beginTransaction(); $currentStmt = $this->pdo->prepare('SELECT id_structure FROM agent WHERE id_agent = :agent_id FOR UPDATE'); $currentStmt->execute(['agent_id' => $agentId]); $currentStructure = $currentStmt->fetchColumn(); if ($currentStructure === false) { throw new \RuntimeException('Agent introuvable.'); } $currentStructureId = $currentStructure !== null ? (int) $currentStructure : null; $newStructureId = $data['structure_id']; $update = $this->pdo->prepare( 'UPDATE agent SET email = :email, telephone = :telephone, adresse = :adresse, est_diplome = :est_diplome, diplome_libelle = :diplome_libelle, date_debut_contrat = :date_debut_contrat, id_poste = :id_poste, type_contrat = :type_contrat, date_fin_contrat = :date_fin_contrat, formation_repartition_annuelle = :formation_repartition_annuelle, commentaire_pta = :commentaire_pta, id_structure = :structure_id WHERE id_agent = :agent_id' ); $update->bindValue(':agent_id', $agentId, PDO::PARAM_INT); $update->bindValue(':email', $data['email']); $update->bindValue(':telephone', $data['telephone']); $update->bindValue(':adresse', $data['adresse']); $update->bindValue(':est_diplome', $data['est_diplome'] ? 1 : 0, PDO::PARAM_INT); $update->bindValue(':diplome_libelle', $data['est_diplome'] ? $data['diplome_libelle'] : null); $update->bindValue(':date_debut_contrat', $data['date_debut_contrat']); $update->bindValue(':id_poste', $data['id_poste'], PDO::PARAM_INT); $update->bindValue(':type_contrat', $data['type_contrat']); $update->bindValue(':date_fin_contrat', $data['date_fin_contrat']); $update->bindValue(':formation_repartition_annuelle', $data['formation_repartition_annuelle'] ? 1 : 0, PDO::PARAM_INT); $update->bindValue(':commentaire_pta', $data['commentaire_pta']); if ($newStructureId === null) { $update->bindValue(':structure_id', null, PDO::PARAM_NULL); } else { $update->bindValue(':structure_id', $newStructureId, PDO::PARAM_INT); } $update->execute(); if ($currentStructureId !== $newStructureId) { $close = $this->pdo->prepare( "UPDATE agent_structure SET actif = FALSE, date_fin = CASE WHEN date_debut IS NOT NULL AND date_debut > CURRENT_DATE THEN date_debut ELSE CURRENT_DATE END WHERE id_agent = :agent_id AND actif = TRUE" ); $close->execute(['agent_id' => $agentId]); if ($newStructureId !== null) { $history = $this->pdo->prepare( "INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif) VALUES (:agent_id, :structure_id, CURRENT_DATE, NULL, TRUE) ON DUPLICATE KEY UPDATE date_fin = NULL, actif = TRUE" ); $history->execute(['agent_id' => $agentId, 'structure_id' => $newStructureId]); } } $this->pdo->commit(); } catch (Throwable $e) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $e; } } public function updateAssignment(int $agentId, ?int $structureId): void { try { $this->pdo->beginTransaction(); $update = $this->pdo->prepare('UPDATE agent SET id_structure = :structure_id WHERE id_agent = :agent_id'); $update->bindValue(':agent_id', $agentId, PDO::PARAM_INT); if ($structureId === null) { $update->bindValue(':structure_id', null, PDO::PARAM_NULL); } else { $update->bindValue(':structure_id', $structureId, PDO::PARAM_INT); } $update->execute(); $close = $this->pdo->prepare( "UPDATE agent_structure SET actif = FALSE, date_fin = CASE WHEN date_debut IS NOT NULL AND date_debut > CURRENT_DATE THEN date_debut ELSE CURRENT_DATE END WHERE id_agent = :agent_id AND actif = TRUE" ); $close->execute(['agent_id' => $agentId]); if ($structureId !== null) { $history = $this->pdo->prepare( "INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif) VALUES (:agent_id, :structure_id, CURRENT_DATE, NULL, TRUE) ON DUPLICATE KEY UPDATE date_fin = NULL, actif = TRUE" ); $history->execute(['agent_id' => $agentId, 'structure_id' => $structureId]); } $this->pdo->commit(); } catch (Throwable $e) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $e; } } public function entriesForWeek(int $agentId, int $year, int $week): array { $stmt = $this->pdo->prepare( "SELECT p.id_planning, p.statut, s2.id_structure, s2.nom AS structure_nom, s2.type_affectation AS structure_type_affectation, c.id_creneau, c.id_motif, c.date_jour, TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut, TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin, m.code AS motif_code, m.libelle AS motif_libelle, m.compte_dans_quota FROM planning p INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine INNER JOIN structure s2 ON s2.id_structure = p.id_structure INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning INNER JOIN motif_planning m ON m.id_motif = c.id_motif WHERE p.id_agent = :agent_id AND sem.annee = :annee AND sem.numero_semaine = :semaine ORDER BY c.date_jour, c.heure_debut, s2.nom" ); $stmt->execute(['agent_id' => $agentId, 'annee' => $year, 'semaine' => $week]); return $stmt->fetchAll(); } public function entriesForDateRange(int $agentId, string $startDate, string $endDate): array { $stmt = $this->pdo->prepare( "SELECT p.id_planning, p.statut, s2.id_structure, s2.nom AS structure_nom, s2.type_affectation AS structure_type_affectation, c.id_creneau, c.id_motif, c.date_jour, TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut, TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin, m.code AS motif_code, m.libelle AS motif_libelle, m.compte_dans_quota FROM planning p INNER JOIN structure s2 ON s2.id_structure = p.id_structure INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning INNER JOIN motif_planning m ON m.id_motif = c.id_motif WHERE p.id_agent = :agent_id AND c.date_jour BETWEEN :date_debut AND :date_fin ORDER BY c.date_jour, c.heure_debut, s2.nom" ); $stmt->execute([ 'agent_id' => $agentId, 'date_debut' => $startDate, 'date_fin' => $endDate, ]); return $stmt->fetchAll(); } public function timeSummaryForRange(int $agentId, string $startDate, string $endDate): array { $stmt = $this->pdo->prepare( "SELECT UPPER(m.code) AS motif_code, m.libelle AS motif_libelle, m.compte_dans_quota, COALESCE(SUM(TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin))), 0) AS minutes_total, COALESCE(SUM(CASE WHEN p.statut = 'VALIDE' THEN TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin)) ELSE 0 END), 0) AS minutes_valides, COALESCE(SUM(CASE WHEN p.statut = 'BROUILLON' THEN TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin)) ELSE 0 END), 0) AS minutes_brouillon FROM planning p INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning INNER JOIN motif_planning m ON m.id_motif = c.id_motif WHERE p.id_agent = :agent_id AND c.date_jour BETWEEN :date_debut AND :date_fin GROUP BY UPPER(m.code), m.libelle, m.compte_dans_quota ORDER BY m.compte_dans_quota DESC, m.libelle" ); $stmt->execute([ 'agent_id' => $agentId, 'date_debut' => $startDate, 'date_fin' => $endDate, ]); return $stmt->fetchAll(); } public function timeSummaryForYear(int $agentId, int $year): array { return $this->timeSummaryForRange($agentId, sprintf('%04d-01-01', $year), sprintf('%04d-12-31', $year)); } }