> */ public function all(?string $schoolYear = null): array { $sql = "SELECT id_periode, libelle, date_debut, date_fin, annee_scolaire, academie, zone, source, date_creation FROM periode_vacance WHERE actif = TRUE"; $params = []; if ($schoolYear !== null && $schoolYear !== '') { $sql .= ' AND annee_scolaire = :annee_scolaire'; $params['annee_scolaire'] = $schoolYear; } $sql .= ' ORDER BY date_debut, libelle'; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } /** * Enregistre un lot de périodes, en INSERT ou mise à jour. * * Le ON DUPLICATE KEY UPDATE s'appuie sur la clé d'unicité du schéma : un * réimport de la même période met à jour son libellé et sa zone au lieu de * la dupliquer, et la RÉACTIVE (`actif = TRUE`) si elle avait été retirée. * Les DATES ne sont volontairement pas mises à jour : ce sont elles qui * identifient la période, les modifier reviendrait à en créer une autre. * * Le lot est transactionnel : soit toutes les périodes sélectionnées entrent, * soit aucune. * * @param array> $periods Déjà validées par le contrôleur. * * @return int Nombre de périodes traitées. */ public function saveMany(array $periods): int { if ($periods === []) { return 0; } $sql = "INSERT INTO periode_vacance (libelle, date_debut, date_fin, annee_scolaire, academie, zone, source, actif) VALUES (:libelle, :date_debut, :date_fin, :annee_scolaire, :academie, :zone, :source, TRUE) ON DUPLICATE KEY UPDATE libelle = VALUES(libelle), zone = VALUES(zone), source = VALUES(source), actif = TRUE"; $stmt = $this->pdo->prepare($sql); $this->pdo->beginTransaction(); try { foreach ($periods as $period) { $stmt->execute([ 'libelle' => $period['libelle'], 'date_debut' => $period['date_debut'], 'date_fin' => $period['date_fin'], 'annee_scolaire' => $period['annee_scolaire'], 'academie' => $period['academie'], 'zone' => $period['zone'], 'source' => $period['source'], ]); } $this->pdo->commit(); } catch (\Throwable $e) { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } throw $e; } return count($periods); } /** * Supprime définitivement une période (et non une désactivation). * * @return bool false si la période n'existait pas. */ public function delete(int $id): bool { $stmt = $this->pdo->prepare('DELETE FROM periode_vacance WHERE id_periode = :id'); $stmt->execute(['id' => $id]); return $stmt->rowCount() > 0; } /** * Périodes actives qui INTERSECTENT l'intervalle demandé. * * Le test `debut <= fin_demandee AND fin >= debut_demande` est un * recouvrement, pas une inclusion : une période à cheval sur les bornes doit * remonter, sans quoi les jours de vacances en début ou fin d'intervalle * seraient qualifiés à tort de périscolaires. * * @return array> */ public function periodsBetween(string $startDate, string $endDate): array { $stmt = $this->pdo->prepare( "SELECT id_periode, libelle, date_debut, date_fin, annee_scolaire, academie, zone, source FROM periode_vacance WHERE actif = TRUE AND date_debut <= :date_fin AND date_fin >= :date_debut ORDER BY date_debut" ); $stmt->execute([ 'date_debut' => $startDate, 'date_fin' => $endDate, ]); return $stmt->fetchAll(); } /** * Retourne le type de période pour les cinq jours ouvrés d'une semaine ISO. * Une date comprise dans une période de vacances confirmée est EXTRASCOLAIRE, * sinon elle est PERISCOLAIRE. * * Toujours cinq entrées, lundi à vendredi : le modèle métier s'arrête au * vendredi. Une semaine peut être MIXTE (vacances commençant en cours de * semaine), c'est à l'appelant d'en tirer les conséquences — * DashboardController::weekPayload() par exemple. * * @return array|null}> */ public function typesForIsoWeek(int $year, int $week): array { $monday = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0); $friday = $monday->modify('+4 days'); $periods = $this->periodsBetween($monday->format('Y-m-d'), $friday->format('Y-m-d')); $days = []; for ($offset = 0; $offset < 5; $offset++) { $date = $monday->modify('+' . $offset . ' days')->format('Y-m-d'); $matchingPeriod = null; foreach ($periods as $period) { if ($date >= $period['date_debut'] && $date <= $period['date_fin']) { $matchingPeriod = $period; break; } } $days[] = [ 'date' => $date, 'type_affectation' => $matchingPeriod ? 'EXTRASCOLAIRE' : 'PERISCOLAIRE', 'vacances' => $matchingPeriod ? [ 'id_periode' => (int) $matchingPeriod['id_periode'], 'libelle' => (string) $matchingPeriod['libelle'], 'annee_scolaire' => (string) $matchingPeriod['annee_scolaire'], ] : null, ]; } return $days; } }