188 lines
6.8 KiB
PHP
188 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use DateTimeImmutable;
|
|
use PDO;
|
|
|
|
/**
|
|
* Périodes de vacances scolaires (`periode_vacance`).
|
|
*
|
|
* Ces périodes déterminent le contexte PÉRISCOLAIRE / EXTRASCOLAIRE de chaque
|
|
* journée : une date incluse dans une période active est extrascolaire, sinon
|
|
* elle est périscolaire. C'est cette qualification qui pilote le filtrage des
|
|
* semaines types (WeekTemplateModel) et le contexte affiché dans l'éditeur.
|
|
*
|
|
* Les périodes peuvent être saisies à la main ou importées depuis
|
|
* data.education.gouv.fr, la colonne `source` conservant l'origine.
|
|
*/
|
|
final class VacationModel extends BaseModel
|
|
{
|
|
/**
|
|
* Périodes actives, éventuellement restreintes à une année scolaire.
|
|
*
|
|
* @param string|null $schoolYear Ex. « 2026-2027 », ou null pour tout.
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
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<int, array<string, mixed>> $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<int, array<string, mixed>>
|
|
*/
|
|
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<int, array{date: string, type_affectation: string, vacances: array<string, mixed>|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;
|
|
}
|
|
}
|