'Lundi', 2 => 'Mardi', 3 => 'Mercredi', 4 => 'Jeudi', 5 => 'Vendredi', ]; public function rulesForStructure(int $structureId): array { $stmt = $this->pdo->prepare( "SELECT id_couverture, id_structure, jour_semaine, TIME_FORMAT(heure_debut, '%H:%i') AS heure_debut, TIME_FORMAT(heure_fin, '%H:%i') AS heure_fin, minimum_agents FROM structure_couverture_horaire WHERE id_structure = :structure_id ORDER BY jour_semaine, heure_debut, heure_fin" ); $stmt->execute(['structure_id' => $structureId]); return $stmt->fetchAll(); } public function replaceRules(int $structureId, array $rules): void { try { $this->pdo->beginTransaction(); $delete = $this->pdo->prepare( 'DELETE FROM structure_couverture_horaire WHERE id_structure = :structure_id' ); $delete->execute(['structure_id' => $structureId]); if ($rules !== []) { $insert = $this->pdo->prepare( 'INSERT INTO structure_couverture_horaire (id_structure, jour_semaine, heure_debut, heure_fin, minimum_agents) VALUES (:structure_id, :jour_semaine, :heure_debut, :heure_fin, :minimum_agents)' ); foreach ($rules as $rule) { $insert->execute([ 'structure_id' => $structureId, 'jour_semaine' => (int) $rule['jour_semaine'], 'heure_debut' => $rule['heure_debut'], 'heure_fin' => $rule['heure_fin'], 'minimum_agents' => (int) $rule['minimum_agents'], ]); } } $this->pdo->commit(); } catch (Throwable $e) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $e; } } public function reportForStructure(int $structureId, int $year, int $weekNumber): array { $rules = $this->rulesForStructure($structureId); $weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0); $weekEnd = $weekStart->modify('+4 days'); if ($rules === []) { return [ 'configured' => false, 'rules' => [], 'gaps' => [], 'validation_warnings' => [], 'summary' => [ 'gap_count' => 0, 'gap_minutes' => 0, 'validation_warning_count' => 0, ], ]; } $entries = $this->workEntries( $structureId, $weekStart->format('Y-m-d'), $weekEnd->format('Y-m-d') ); $entriesByDate = []; foreach ($entries as $entry) { $entriesByDate[$entry['date_jour']][] = $entry; } $gaps = []; $validationWarnings = []; foreach ($rules as $rule) { $dayNumber = (int) $rule['jour_semaine']; $date = $weekStart->modify('+' . ($dayNumber - 1) . ' days')->format('Y-m-d'); $start = $this->timeToMinutes((string) $rule['heure_debut']); $end = $this->timeToMinutes((string) $rule['heure_fin']); $minimum = (int) $rule['minimum_agents']; for ($slotStart = $start; $slotStart < $end; $slotStart += 15) { $slotEnd = min($slotStart + 15, $end); $plannedAgents = []; $validatedAgents = []; foreach ($entriesByDate[$date] ?? [] as $entry) { $entryStart = $this->timeToMinutes((string) $entry['heure_debut']); $entryEnd = $this->timeToMinutes((string) $entry['heure_fin']); if ($entryStart >= $slotEnd || $entryEnd <= $slotStart) { continue; } $agentId = (int) $entry['id_agent']; $plannedAgents[$agentId] = true; if ($entry['statut'] === 'VALIDE') { $validatedAgents[$agentId] = true; } } $plannedCount = count($plannedAgents); $validatedCount = count($validatedAgents); $base = [ 'date' => $date, 'jour_semaine' => $dayNumber, 'jour_libelle' => self::DAY_LABELS[$dayNumber] ?? 'Jour', 'heure_debut' => $this->minutesToTime($slotStart), 'heure_fin' => $this->minutesToTime($slotEnd), 'minimum_agents' => $minimum, 'agents_planifies' => $plannedCount, 'agents_valides' => $validatedCount, ]; if ($plannedCount < $minimum) { $base['agents_manquants'] = $minimum - $plannedCount; $this->appendMergedInterval($gaps, $base, ['minimum_agents', 'agents_planifies', 'agents_manquants']); } elseif ($validatedCount < $minimum) { $base['validations_manquantes'] = $minimum - $validatedCount; $this->appendMergedInterval($validationWarnings, $base, ['minimum_agents', 'agents_valides', 'validations_manquantes']); } } } $gapMinutes = 0; foreach ($gaps as $gap) { $gapMinutes += $this->timeToMinutes($gap['heure_fin']) - $this->timeToMinutes($gap['heure_debut']); } return [ 'configured' => true, 'rules' => $rules, 'gaps' => $gaps, 'validation_warnings' => $validationWarnings, 'summary' => [ 'gap_count' => count($gaps), 'gap_minutes' => $gapMinutes, 'validation_warning_count' => count($validationWarnings), ], ]; } private function workEntries(int $structureId, string $dateStart, string $dateEnd): array { $stmt = $this->pdo->prepare( "SELECT p.id_agent, p.statut, c.date_jour, TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut, TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin FROM planning p INNER JOIN agent a ON a.id_agent = p.id_agent AND a.actif = TRUE 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_structure = :structure_id AND c.date_jour BETWEEN :date_start AND :date_end AND m.code = 'TRAVAIL' ORDER BY c.date_jour, c.heure_debut, c.heure_fin" ); $stmt->execute([ 'structure_id' => $structureId, 'date_start' => $dateStart, 'date_end' => $dateEnd, ]); return $stmt->fetchAll(); } private function appendMergedInterval(array &$target, array $slot, array $comparisonKeys): void { $lastIndex = count($target) - 1; if ($lastIndex >= 0) { $last = $target[$lastIndex]; $same = $last['date'] === $slot['date'] && $last['heure_fin'] === $slot['heure_debut']; foreach ($comparisonKeys as $key) { $same = $same && (int) $last[$key] === (int) $slot[$key]; } if ($same) { $target[$lastIndex]['heure_fin'] = $slot['heure_fin']; return; } } $target[] = $slot; } private function timeToMinutes(string $time): int { [$hours, $minutes] = array_map('intval', explode(':', substr($time, 0, 5))); return ($hours * 60) + $minutes; } private function minutesToTime(int $minutes): string { return sprintf('%02d:%02d', intdiv($minutes, 60), $minutes % 60); } }