> */ public function quotaTracking(int $year): array { $profiles = $this->quotaProfiles($year); $rows = $this->allocatedMinutes($year); $tracking = []; foreach ($rows as $row) { $agentId = (int) $row['id_agent']; $profile = $profiles[$agentId] ?? [ 'quotite_travail' => 100.0, 'quota_cible_minutes' => QuotaModel::REFERENCE_MINUTES, 'annee' => $year, ]; $target = (int) $profile['quota_cible_minutes']; $validated = (int) $row['minutes_valides']; $draft = (int) $row['minutes_brouillon']; $allocated = $validated + $draft; $tracking[] = [ 'id_agent' => $agentId, 'matricule' => (string) ($row['matricule'] ?? ''), 'nom' => (string) $row['nom'], 'prenom' => (string) $row['prenom'], 'structure_nom' => $row['structure_nom'] !== null ? (string) $row['structure_nom'] : null, 'poste_libelle' => $row['poste_libelle'] !== null ? (string) $row['poste_libelle'] : null, 'quotite_travail' => (float) $profile['quotite_travail'], 'quota_cible_minutes' => $target, 'quota_source_annee' => (int) $profile['annee'], 'quota_estime' => !isset($profiles[$agentId]), 'minutes_valides' => $validated, 'minutes_brouillon' => $draft, 'minutes_affectees' => $allocated, 'minutes_restantes' => $target - $allocated, 'taux_consommation' => $target > 0 ? round(($allocated / $target) * 100, 1) : 0.0, ]; } return $tracking; } /** * Minutes décomptées de l'année civile, par agent actif. * * @return array> */ private function allocatedMinutes(int $year): array { $stmt = $this->pdo->prepare( "SELECT a.id_agent, a.matricule, a.nom, a.prenom, s.nom AS structure_nom, po.libelle AS poste_libelle, COALESCE(SUM(CASE WHEN p.statut = 'VALIDE' AND m.id_motif IS NOT NULL 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' AND m.id_motif IS NOT NULL 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 agent a LEFT JOIN structure s ON s.id_structure = a.id_structure LEFT JOIN poste_agent po ON po.id_poste = a.id_poste LEFT JOIN planning p ON p.id_agent = a.id_agent LEFT JOIN creneau_horaire c ON c.id_planning = p.id_planning AND c.date_jour BETWEEN :date_debut AND :date_fin LEFT JOIN motif_planning m ON m.id_motif = c.id_motif AND m.compte_dans_quota = TRUE WHERE a.actif = TRUE GROUP BY a.id_agent, a.matricule, a.nom, a.prenom, s.nom, po.libelle ORDER BY a.nom, a.prenom" ); $stmt->execute([ 'date_debut' => sprintf('%04d-01-01', $year), 'date_fin' => sprintf('%04d-12-31', $year), ]); return $stmt->fetchAll(); } /** * Quota retenu pour chaque agent : année exacte si elle existe, sinon * l'année passée la plus proche, sinon l'année future la plus proche. * Même ordre de préférence que QuotaModel::quotaProfile(). * * @return array> */ private function quotaProfiles(int $year): array { $stmt = $this->pdo->prepare( 'SELECT id_agent, annee, quotite_travail, quota_cible_minutes FROM ( SELECT q.id_agent, q.annee, q.quotite_travail, q.quota_cible_minutes, ROW_NUMBER() OVER ( PARTITION BY q.id_agent ORDER BY CASE WHEN q.annee = :year_exact THEN 0 WHEN q.annee < :year_past THEN 1 ELSE 2 END, CASE WHEN q.annee <= :year_latest THEN q.annee END DESC, CASE WHEN q.annee > :year_future THEN q.annee END ASC ) AS rang FROM quota_agent_annuel q ) AS classement WHERE rang = 1' ); $stmt->execute([ 'year_exact' => $year, 'year_past' => $year, 'year_latest' => $year, 'year_future' => $year, ]); $profiles = []; foreach ($stmt->fetchAll() as $row) { $profiles[(int) $row['id_agent']] = [ 'annee' => (int) $row['annee'], 'quotite_travail' => (float) $row['quotite_travail'], 'quota_cible_minutes' => (int) $row['quota_cible_minutes'], ]; } return $profiles; } /** * Nombre de brouillons contenant au moins une affectation. * Requête volontairement légère : elle alimente la pastille de navigation * rechargée sur chaque page. */ public function pendingCount(): int { $stmt = $this->pdo->query( "SELECT COUNT(*) FROM ( SELECT p.id_planning FROM planning p INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning WHERE p.statut = 'BROUILLON' GROUP BY p.id_planning ) AS brouillons_servis" ); return (int) $stmt->fetchColumn(); } /** * Nombre d'agents actifs et de lieux actifs, pour situer les compteurs. * * @return array{agents: int, structures: int} */ public function scope(): array { return [ 'agents' => (int) $this->pdo->query('SELECT COUNT(*) FROM agent WHERE actif = TRUE')->fetchColumn(), 'structures' => (int) $this->pdo->query('SELECT COUNT(*) FROM structure WHERE actif = TRUE')->fetchColumn(), ]; } }