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 )' ); $stmt->execute([ 'agent_id' => $agentId, 'annee' => $year, 'quotite' => $rate, 'quota_reference' => self::REFERENCE_MINUTES, 'quota_cible' => $target, 'commentaire' => 'Quota PTA de référence pour l’année civile', ]); } /** * Retourne le quota de l'année civile contenant la date fournie. * * Exemple : une date de contexte au 15/09/2026 renvoie toujours la période * 01/01/2026 -> 31/12/2026, quelle que soit la date de début du contrat. */ public function forDate(int $agentId, string $contextDate): array { $period = $this->referencePeriod($agentId, $contextDate); $quota = $this->quotaProfile($agentId, (int) $period['annee_periode']); $usedStmt = $this->pdo->prepare( "SELECT 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 AND m.compte_dans_quota = TRUE" ); $usedStmt->execute([ 'agent_id' => $agentId, 'date_debut' => $period['date_debut_periode'], 'date_fin' => $period['date_fin_periode'], ]); $used = $usedStmt->fetch() ?: ['minutes_valides' => 0, 'minutes_brouillon' => 0]; $validated = (int) $used['minutes_valides']; $draft = (int) $used['minutes_brouillon']; $target = (int) $quota['quota_cible_minutes']; $allocated = $validated + $draft; return array_merge($period, [ 'annee' => (int) $period['annee_periode'], 'quotite_travail' => (float) $quota['quotite_travail'], 'quota_reference_minutes' => (int) $quota['quota_reference_minutes'], 'quota_cible_minutes' => $target, 'quota_source_annee' => (int) $quota['quota_source_annee'], 'minutes_valides' => $validated, 'minutes_brouillon' => $draft, 'minutes_affectees' => $allocated, 'minutes_restantes' => $target - $allocated, 'taux_consommation' => $target > 0 ? round(($allocated / $target) * 100, 2) : 0, ]); } public function forDateForApi(int $agentId, string $contextDate): array { return $this->enrich($this->forDate($agentId, $contextDate)); } public function annual(int $agentId, int $year): array { return $this->forDate($agentId, sprintf('%04d-01-01', $year)); } public function annualForApi(int $agentId, int $year): array { return $this->forDateForApi($agentId, sprintf('%04d-01-01', $year)); } /** * Période de référence PTA : toujours l'année civile. * La date de contrat est retournée uniquement à titre informatif. */ public function referencePeriod(int $agentId, string $contextDate): array { $context = DateTimeImmutable::createFromFormat('!Y-m-d', $contextDate); if (!$context || $context->format('Y-m-d') !== $contextDate) { throw new InvalidArgumentException('Date de référence du quota invalide.'); } $stmt = $this->pdo->prepare( 'SELECT date_debut_contrat FROM agent WHERE id_agent = :agent_id LIMIT 1' ); $stmt->execute(['agent_id' => $agentId]); $contractStartValue = $stmt->fetchColumn(); $year = (int) $context->format('Y'); $start = new DateTimeImmutable(sprintf('%04d-01-01', $year)); $end = new DateTimeImmutable(sprintf('%04d-12-31', $year)); return [ 'date_reference' => $context->format('Y-m-d'), 'date_debut_contrat' => $contractStartValue ? (string) $contractStartValue : null, 'date_debut_periode' => $start->format('Y-m-d'), 'date_fin_periode' => $end->format('Y-m-d'), 'annee_periode' => $year, 'periode_libelle' => sprintf('année civile %d · du %s au %s', $year, $start->format('d/m/Y'), $end->format('d/m/Y')), 'periode_contractuelle' => false, 'periode_civile' => true, // Conservé pour compatibilité avec les anciens écrans : la date de // contrat n'est plus requise pour calculer la période du quota. 'date_contrat_manquante' => false, ]; } public function enrich(array $quota): array { return array_merge($quota, [ 'quota_cible' => $this->formatMinutes((int) $quota['quota_cible_minutes']), 'valides' => $this->formatMinutes((int) $quota['minutes_valides']), 'brouillons' => $this->formatMinutes((int) $quota['minutes_brouillon']), 'affectees' => $this->formatMinutes((int) $quota['minutes_affectees']), 'restantes' => $this->formatMinutes((int) $quota['minutes_restantes']), ]); } public function formatMinutes(int $minutes): array { $sign = $minutes < 0 ? -1 : 1; $absolute = abs($minutes); $hours = intdiv($absolute, 60); $mins = $absolute % 60; return [ 'minutes' => $minutes, 'heures_decimales' => round($minutes / 60, 2), 'libelle' => ($sign < 0 ? '-' : '') . $hours . ' h ' . str_pad((string) $mins, 2, '0', STR_PAD_LEFT), ]; } private function quotaProfile(int $agentId, int $year): array { $stmt = $this->pdo->prepare( "SELECT annee, quotite_travail, quota_reference_minutes, quota_cible_minutes FROM quota_agent_annuel WHERE id_agent = :agent_id ORDER BY CASE WHEN annee = :year_exact THEN 0 WHEN annee < :year_past THEN 1 ELSE 2 END, CASE WHEN annee <= :year_latest THEN annee END DESC, CASE WHEN annee > :year_future THEN annee END ASC LIMIT 1" ); $stmt->execute([ 'agent_id' => $agentId, 'year_exact' => $year, 'year_past' => $year, 'year_latest' => $year, 'year_future' => $year, ]); $quota = $stmt->fetch(); if (!$quota) { return [ 'quotite_travail' => 100.00, 'quota_reference_minutes' => self::REFERENCE_MINUTES, 'quota_cible_minutes' => self::REFERENCE_MINUTES, 'quota_source_annee' => $year, ]; } return [ 'quotite_travail' => (float) $quota['quotite_travail'], 'quota_reference_minutes' => (int) $quota['quota_reference_minutes'], 'quota_cible_minutes' => (int) $quota['quota_cible_minutes'], 'quota_source_annee' => (int) $quota['annee'], ]; } }