Files
pta/docs
2026-08-25 15:12:11 +02:00
..
2026-08-20 17:00:06 +02:00
2026-08-20 17:00:06 +02:00
2026-08-20 17:00:06 +02:00
2026-08-20 17:00:06 +02:00
2026-08-20 17:00:06 +02:00
2026-08-25 15:12:11 +02:00
2026-08-25 15:12:11 +02:00

PTA — Planning de Travail Annualisé

Documentation technique de l'application. Elle s'adresse aux développeurs qui reprennent le projet : elle explique ce que fait l'application, comment elle est construite, et documente l'intégralité des points d'entrée HTTP.

Les fichiers README_V7_*.md de ce dossier documentent chacun une version successive et servent d'historique des décisions. Ce document-ci décrit l'état courant du code. En cas de divergence, seul le code fait foi.


Sommaire

  1. Ce que fait l'application
  2. Contraintes techniques structurantes
  3. Démarrage
  4. Architecture
  5. Rôles, authentification et droits
  6. Journalisation
  7. Sécurité — contraintes à respecter
  8. JavaScript
  9. Modèle métier
  10. Piège : logique de validation dupliquée
  11. Les pages
  12. Référence des endpoints API · Planning · Semaines types · Agents · PDF · Lieux & couverture · Annuel & PTA · Équipes · Vacances · Tableau de bord · Comptes délégués · Espace DSI
  13. Base de données
  14. Où chercher quoi
  15. Checklist avant commit
  16. Historique des versions

1. Ce que fait l'application

PTA gère la planification annualisée des agents d'accueils de loisirs périscolaires et extrascolaires d'une collectivité.

Le principe métier : un agent ne travaille pas un nombre d'heures fixe chaque semaine, mais un volume annuel (1 607 h de référence, proratisé par sa quotité de travail). Les semaines sont donc inégales — beaucoup d'heures pendant les vacances scolaires, moins pendant le temps scolaire — et l'application sert à composer ces semaines, à vérifier en continu que le cumul reste cohérent avec le volume dû, et à valider le résultat.

L'application permet de :

  • saisir semaine par semaine les créneaux d'un agent sur un lieu (planning.php) ;
  • composer des semaines types réutilisables et les appliquer sur toute une période ;
  • suivre le quota annuel consommé, mois par mois et sur l'année (annual.php, pta.php) ;
  • contrôler la couverture : les plages horaires d'un lieu où il manque des agents ;
  • faire valider les plannings par le Service Enfance, avec des contrôles bloquants ;
  • composer les équipes des mercredis et des vacances, avec taux d'encadrement ;
  • importer le calendrier des vacances scolaires depuis l'API officielle ;
  • éditer des PDF (planning d'agent, planning de lieu, bilan d'heures, contrat horaire) ;
  • administrer les comptes d'accès (jetons) et consulter le journal d'activité.

2. Contraintes techniques structurantes

Aucune dépendance externe. Pas de Composer, pas de npm, pas de framework, pas de bundler, pas de suite de tests. Uniquement PHP 8.3 + PDO/MariaDB + JavaScript vanilla. Le générateur PDF (app/Services/SimplePdf.php) et l'autoloader (app/autoload.php) sont volontairement réécrits à la main. N'introduisez pas de dépendance sans raison forte.

Tout est en français : identifiants SQL (creneau_horaire, id_agent, statut), messages utilisateur, commentaires, noms de méthodes métier. Conservez cette langue pour tout nouveau code.

Pas de routeur. Chaque URL correspond à un fichier PHP réel de 5 à 10 lignes, à la racine ou dans api/.


3. Démarrage

cp .env.example .env      # puis renseigner DB_PASSWORD / DB_ROOT_PASSWORD
docker compose up -d --build

L'application est alors sur http://localhost:8080, phpMyAdmin sur http://127.0.0.1:8081.

Services docker-compose

Service Rôle
db MariaDB 11.4. Le schéma database/pta_prod_schema_mariadb.sql n'est joué qu'à la création initiale du volume.
db-migrate Conteneur jetable qui applique les database/migration_*.sql à chaque démarrage.
php php-fpm 8.3. Attend la sortie en succès de db-migrate : une migration en échec empêche l'application de tourner sur une base incomplète.
nginx Sert les pages PHP et les assets statiques. Publié sur APP_PORT.
phpmyadmin N'écoute que sur 127.0.0.1 et exige un identifiant MariaDB.

⚠️ Le code est COPIÉ dans l'image, pas monté

Toute modification PHP/JS/CSS exige un rebuild des deux services — nginx sert lui-même les assets statiques depuis sa propre copie de /var/www/html :

docker compose up -d --build php nginx

Commandes utiles

docker compose logs -f php nginx db
docker compose logs db-migrate       # compte rendu de l'application des migrations
docker compose exec db mariadb -upta -p pta
docker compose down                  # arrêt en conservant les données
docker compose down -v               # DÉTRUIT le volume MariaDB

# Vérification syntaxique (il n'existe ni linter ni tests automatisés)
find . -name '*.php' -not -path './.git/*' -exec php -l {} \;

Après une installation neuve

  1. Se connecter avec le jeton DSI d'installation PTA-INSTALLATION-DSI, écrit en clair dans database/migration_v7_9e_dsi_comptes_journal.sql — il est donc public.
  2. Le régénérer immédiatement depuis la page « Accès & journaux ». L'écran alerte tant que ce n'est pas fait.
  3. Générer les jetons manquants pour équiper les agents et les responsables de lieu.
  4. Changer le mot de passe MariaDB du .env, encore à sa valeur d'exemple.

Migrations

Les fichiers database/migration_*.sql sont appliqués automatiquement par db-migrate (docker/db/apply-migrations.sh), sur base neuve comme sur base existante. Le script tient un registre schema_migration (fichier + empreinte MD5) : un fichier déjà appliqué et inchangé est ignoré, un fichier modifié est rejoué.

Deux règles :

  • Toute migration doit être idempotente : CREATE TABLE IF NOT EXISTS, CREATE OR REPLACE VIEW, INSERT … ON DUPLICATE KEY UPDATE.
  • Les fichiers sont pris dans l'ordre alphabétique. Nommer une migration migration_v7_10_… la ferait passer avant migration_v7_9_…. C'est pourquoi les migrations récentes suivent le préfixe de la précédente (…_v7_9e_, …_v7_9f_, …_v7_9g_, …_v7_9h_) et non le numéro de version fonctionnelle.

PTA_DB_PORT, DB_PORT et PMA_PORT du compose sont les ports joints à l'intérieur du réseau Docker, où MariaDB écoute sur 3306. Ce ne sont pas les ports publiés sur l'hôte (APP_PORT, PMA_PORT de .env) : les modifier coupe l'accès à la base.


4. Architecture

4.1 Arborescence

/                          points d'entrée « pages » (planning.php, agents.php, …)
├── api/                   points d'entrée JSON et PDF
├── app/
│   ├── autoload.php       autoloader PSR-4 maison
│   ├── bootstrap.php      session + PDO + contrôle de schéma + journal
│   ├── Controllers/       logique applicative, gardes, validation
│   ├── Core/              Access, Csrf, Journal, JsonResponse, Request, Security, Session…
│   ├── Models/            SQL PDO uniquement, aucune règle d'accès
│   ├── Services/          appels d'API externes et génération PDF
│   └── Views/
│       ├── layouts/       header.php (nav) et footer.php (injection JS)
│       ├── pages/         une vue par page
│       └── partials/      markup réutilisable
├── assets/                style.css et js/ (une IIFE par module)
├── database/              schéma initial + migrations
├── docker/                Dockerfiles, conf nginx, script de migration
└── docs/                  cette documentation

4.2 Deux types de points d'entrée

Pages HTML — fichiers à la racine :

require __DIR__ . '/page_bootstrap.php';
runPage('planning');   // -> PageController::planning()

page_bootstrap.php attrape tout Throwable et rend une page d'erreur HTML autonome ; le détail technique reste journalisé côté serveur par Security::reportThrowable().

Endpoints JSON/PDF — api/*.php, plus save_planning.php, validate_planning.php et reopen_planning.php restés à la racine pour raisons historiques :

JsonResponse::bootstrap();                       // handlers d'erreur -> JSON, jamais du HTML
$pdo = require dirname(__DIR__) . '/app/bootstrap.php';
(new PlanningController($pdo))->show();

JsonResponse::bootstrap() convertit warnings et exceptions en réponses JSON 500 — indispensable, car le JS refuse toute réponse non-JSON. Les endpoints PDF ne l'appellent pas : ils émettent des octets binaires via PdfResponse::inline() et sortent leurs erreurs en texte brut.

4.3 Chaîne d'un appel

api/x.php  → Controller (Access + Csrf + Request) → Model (SQL PDO) → JsonResponse::send()
page.php   → PageController → Models → Views/pages/x.php
                                        ├── layouts/header.php   (nav filtrée par rôle)
                                        ├── partials/x.php       (markup)
                                        └── layouts/footer.php   (window.PTA_CONFIG + scripts)

app/bootstrap.php démarre la session, ouvre le PDO (db.php ← config.php ← variables d'environnement PTA_DB_*), appelle SchemaModel::assertCurrent() à chaque requête et pose la connexion du journal.

Ajouter une table ou une colonne obligatoire impose de mettre à jour les listes de SchemaModel, sinon l'application entière refuse de démarrer — ou, pire, démarre sans le contrôle.

4.4 Ajouter un endpoint

  1. Créer le fichier mince dans api/ (voir modèle ci-dessus).
  2. Ajouter la méthode au contrôleur, avec dans cet ordre : garde Access → Request::requireMethod('POST') → Request::json() → Csrf::assertPayload($payload) → validation → modèle → Journal::ecriture() → JsonResponse::send().

4.5 Ajouter une page

  1. Fichier PHP à la racine (require page_bootstrap.php + runPage('cle')).
  2. Méthode dans PageController — son nom EST la clé de page utilisée par acces_role_page (d'où manage_agents() en snake_case, à rebours du reste du projet).
  3. Vue app/Views/pages/cle.php + partiel app/Views/partials/cle.php.
  4. Module JS, déclaré dans les tableaux scripts / modules de la méthode.
  5. Inscription de la clé dans $navigation et dans un groupe de $navigationGroups (app/Views/layouts/header.php).
  6. Migration SQL insérant la ligne dans acces_role_page : les pages autorisées sont lues en base, pas écrites en PHP.

Une page absente des groupes reste affichée dans un groupe « Autres » : ce filet évite qu'un oubli la rende inatteignable.

Seule exception assumée : faq (guide d'utilisation) est volontairement absente de $navigation et rendue par le bloc .view-tab-help ancré à droite de la barre. L'omettre de $navigation est ce qui l'empêche d'être aussi repêchée par « Autres » et d'apparaître deux fois. Elle reste soumise à acces_role_page (migration_v7_9d_faq.sql).


5. Rôles, authentification et droits

5.1 Les quatre rôles

Rôle Périmètre
AGENT Lecture seule sur son propre planning.
RESPONSABLE_STRUCTURE Édition des brouillons de son seul lieu.
SERVICE_ENFANCE Tout le métier ; seul habilité à valider. Distribue les jetons de terrain.
DSI Administration technique : comptes d'accès et journal. Aucun droit métier.

5.2 Authentification par jeton (V7.11)

role.php n'est pas un sélecteur de rôle. Une session ne peut naître que de Access::openSession(), appelée après vérification d'un jeton par CompteModel::authentifier(). Le rôle, l'agent et le lieu sont lus sur le compte, jamais déclarés par l'utilisateur. La base ne conserve qu'une empreinte SHA-256 du jeton.

Access::select() a disparu — ne pas le réintroduire.

5.3 Second facteur pour les comptes privilégiés (V7.12)

Les comptes SERVICE_ENFANCE et DSI exigent, après le jeton, un mot de passe choisi par le titulaire à sa première connexion et haché par password_hash(). Entre les deux étapes, la session ne porte qu'une attente horodatée de cinq minutes : un jeton validé sans mot de passe n'ouvre rien.

La règle « qui doit avoir un mot de passe » est portée par la vue (v_acces_compte.mot_de_passe_requis), pas par du PHP. La DSI peut effacer un mot de passe, jamais en poser un — sans quoi elle connaîtrait celui de tout le monde.

Un frein anti-force-brute compte les échecs récents via JournalModel::echecsRecents() : 10 échecs sur 15 minutes bloquent. Renommer les actions JETON_REFUSE ou MOT_DE_PASSE_REFUSE désarmerait ce frein silencieusement.

5.4 Délégation des jetons de terrain (V7.27)

Le Service Enfance distribue lui-même les jetons des rôles AGENT et RESPONSABLE_STRUCTURE, depuis l'onglet « Administration » (AccessAccountController, api/acces_*.php).

La délégation s'arrête à ces deux rôles : un compte SERVICE_ENFANCE créé par le Service Enfance serait une auto-promotion, un compte DSI lui ouvrirait le journal de ses propres actions. La borne est posée des deux côtés — AccessAccountController::ROLES_DELEGUES en PHP, can_manage_field_access en base — et ne doit être élargie ni d'un côté ni de l'autre. La DSI reste seule sur les comptes privilégiés, les mots de passe et le journal.

5.5 Relecture du compte à chaque requête

Access relit le compte à chaque requête (v_acces_compte, filtrée sur actif et identite_active) : c'est ce qui fait qu'un jeton révoqué, un agent désactivé ou un lieu fermé referment la session en cours. Ne pas mettre ce résultat en cache entre deux requêtes.

5.6 Les droits vivent en SQL, pas en PHP

Access.php ne contient aucune règle de droit. Tout est lu dans les vues créées par database/migration_v7_8_securite_acces.sql :

v_acces_role_page, v_acces_role_permission, v_acces_identite_agent, v_acces_identite_structure, v_acces_agent_par_structure, v_acces_perimetre_creneau, v_acces_perimetre_planning — plus v_acces_compte, v_acces_compte_manquant et v_journal_activite (V7.11).

Modifier un droit se fait en SQL dans acces_role_page / acces_role_permission, sans redéploiement. Une permission absente de la base vaut « refusé ».

SchemaModel vérifie au démarrage la présence des dix vues et fait échouer l'application si une migration manque — c'est volontairement fail-closed, et docker/db/apply-migrations.sh refait le même contrôle au déploiement.

Permissions actuelles :

Permission Ce qu'elle ouvre
read_only Marque un rôle en lecture seule (AGENT).
can_edit_draft Éditer les brouillons — garde requireDraftEditor().
can_edit_cross_structure Sortir du périmètre d'un seul lieu.
can_validate Valider un planning.
can_reopen Repasser un planning validé en brouillon.
can_view_pending Voir la file d'attente de validation.
can_use_templates Semaines types (conditionne aussi le chargement des scripts).
can_copy_full_week Recopier une semaine entière d'un agent.
can_forecast_next_year Projeter sur l'année suivante.
can_manage_agents Référentiel des agents.
can_manage_structures Référentiel des lieux.
can_manage_coverage Règles de couverture.
can_manage_teams Équipes des mercredis et vacances.
can_manage_vacations Calendrier des vacances scolaires.
can_manage_pta Contraintes et souhaits du PTA.
can_manage_access Comptes d'accès, tous rôles (DSI).
can_manage_field_access Comptes d'accès de terrain uniquement (Service Enfance, V7.27).
can_read_journal Journal d'activité (DSI).

5.7 Les gardes de contrôleur

Les contrôles sont doublés : l'UI masque, mais le serveur tranche. Toute méthode de contrôleur commence par un garde. Ces gardes émettent directement un 401/403 JSON et coupent l'exécution — l'appelant n'a donc pas à tester leur retour.

Garde Ce qu'il vérifie
requirePage($cle) Page autorisée pour le rôle (acces_role_page). Pages HTML uniquement.
requireRoles([…]) Appartenance à une liste de rôles.
requireService() Rôle SERVICE_ENFANCE.
requireDsi() Rôle DSI.
requireDraftEditor() Permission can_edit_draft.
requireStructureView($id) Droit de consulter ce lieu.
requireStructureEdit($id) Droit d'éditer ce lieu (un responsable est borné au sien).
requireAgentView($id) Droit de consulter cet agent (un AGENT est borné à lui-même).
requireEntryEdit($id) Droit d'éditer ce créneau (v_acces_perimetre_creneau).
requirePlanningEdit($id) Droit d'éditer ce planning (v_acces_perimetre_planning).

Ne jamais ajouter un endpoint mutant sans garde ni sans Csrf::assertPayload($payload).

5.8 CSRF

Le jeton voyage dans le corps JSON (csrf_token), pas dans un en-tête — ce choix évite la requête préliminaire CORS et garde les appels fetch() identiques d'un module à l'autre.

csrf_token: window.PTA_CONFIG.csrfToken

Il vit aussi longtemps que la session : il n'est pas renouvelé à chaque requête, sans quoi deux onglets ouverts s'invalideraient l'un l'autre. La comparaison passe par hash_equals().

role.php fait exception et vérifie son CSRF à la main : les données y arrivent en formulaire classique et un échec doit produire une page HTML, pas du JSON.


6. Journalisation

App\Core\Journal écrit dans journal_activite. Sa connexion PDO est posée une fois par app/bootstrap.php — les appelants n'ont rien à transporter :

Journal::ecriture('PLANNING_VALIDE', 'planning', $planningId, 'Planning validé pour…', [contexte]);

Cinq catégories : CONNEXION, ECRITURE, ACCES_REFUSE, ADMINISTRATION, SYSTEME.

Trois règles :

  1. Tout endpoint mutant doit journaliser son succès, après l'écriture en base et avant le JsonResponse::send() — au même titre qu'il porte un garde et un Csrf::assertPayload().
  2. Les lectures ne sont jamais journalisées (choix de volume). Les refus le sont, et Access comme Csrf s'en chargent déjà — inutile de les doubler dans les contrôleurs.
  3. Le journal ne doit jamais faire échouer l'action : Journal avale ses propres erreurs. Ne pas entourer un appel de logique qui pourrait lever.

Actions existantes (extrait) : CONNEXION_REUSSIE, JETON_REFUSE, MOT_DE_PASSE_REFUSE, CONNEXION_BLOQUEE, DECONNEXION, COMPTE_CREE, JETON_REGENERE, JETONS_GENERES_EN_LOT, MOT_DE_PASSE_REINITIALISE, JOURNAL_PURGE, PLANNING_ENREGISTRE, PLANNING_VALIDE, PLANNING_ROUVERT, CRENEAU_MODIFIE, CRENEAU_SUPPRIME, SEMAINE_COPIEE, SEMAINE_TYPE_APPLIQUEE_PERIODE, AGENT_CREE, AGENT_MODIFIE, STRUCTURE_CREEE, COUVERTURE_ENREGISTREE, VACANCES_ENREGISTREES, EQUIPE_CREEE, CSRF_INVALIDE, ROLE_HORS_PERIMETRE, COMPTE_HORS_PERIMETRE.


7. Sécurité — contraintes à respecter

  • CSP à nonce (app/Core/Security.php) : script-src 'self' 'nonce-…', sans unsafe-inline. Tout <script> inline ajouté à une vue doit porter nonce="<?= $cspNonce ?>". Aucun onclick= ni style= inline ne fonctionnera.
  • PTA.utils.escapeHtml échappe aussi les guillemets et vaut donc en contexte texte comme en contexte attribut. Ne pas la réimplémenter localement : une variante incomplète avait introduit un XSS stocké.
  • Ordre des blocs nginx : nginx retient la première regex correspondante. Tout nouveau refus doit être placé avant location ~ \.php$, sinon il est sans effet. Sont déjà refusés : /app/, /database/, /docker/, les fichiers d'amorçage (config.php, db.php, page_bootstrap.php), les README* et les extensions .env .sql .ini .log .bak .dump .yml .yaml .md. Ces règles doivent suivre toute réorganisation de l'arborescence.
  • PTA_DEBUG reste à 0. Les détails d'exception passent par Security::reportThrowable(), qui journalise côté serveur et ne renvoie rien au client.
  • session_start() ne doit jamais être appelé directement : passer par App\Core\Session::start().
  • Ne jamais redéfinir PMA_USER / PMA_PASSWORD dans le compose : cela rouvre phpMyAdmin sans mot de passe.

8. JavaScript

Pas de bundler, pas de module ES. Chaque fichier est une IIFE qui lit le global window.PTA publié par core.js (config, elements, state, shared, utils, modules) et s'enregistre :

PTA.modules.agentOverview = { init, load };

Ordre de chargement imposé : core.js d'abord (il publie window.PTA), puis les modules de la page, puis page-bootstrap.js en dernier (il appelle leurs init()).

PageController décide quels scripts et quels modules charger (tableaux scripts / modules), footer.php les injecte avec deux globales :

  • window.PTA_CONFIG — jeton CSRF, profil d'accès, permissions, référentiel des motifs (valable pour toute l'application) ;
  • window.PTA_PAGE_CONFIG — modules à initialiser et paramètres d'URL déjà assainis.

page-bootstrap.js applique ensuite les filtres d'URL (structure_id, agent_id, week, month, focus). Les modules se rafraîchissent entre eux via l'appel optionnel PTA.modules.autre?.refresh?.({ silent: true }).

⚠️ Versionnage manuel des assets

Les assets sont versionnés à la main : assets/style.css?v=7.29 et assets/js/*.js?v=7.29. Bumper ce numéro aux trois endroits après toute modification de JS/CSS :

  1. app/Views/layouts/header.php
  2. app/Views/layouts/footer.php
  3. app/Views/pages/role.php — qui porte sa propre balise <head>

Sinon les navigateurs et le expires 7d de nginx servent l'ancienne version.

Modules disponibles

Fichier Module Pages
core.js (socle) toutes
page-bootstrap.js (amorçage) toutes
dashboard.js dashboard dashboard
planning-editor.js planning planning
week-templates.js, week-template-editor.js weekTemplates, weekTemplateEditor planning, si can_use_templates
pending-validation.js pendingValidation pending
coverage.js coverage coverage, administration
structure-overview.js structureOverview structures
agent-overview.js agentOverview agents
annual-overview.js annualOverview annual
pta.js pta pta
teams.js teams teams
vacation-calendar.js vacationCalendar vacations
administration.js administration manage_agents, manage_structures
address-autocomplete.js, field-format.js addressAutocomplete, fieldFormat manage_agents
agent-access.js agentAccess manage_agents
access-tokens.js accessTokens administration
dsi.js dsi dsi

9. Modèle métier

9.1 Le quota annuel

  • Il suit l'année civile (01/01 → 31/12), jamais l'année scolaire ni la date de contrat.
  • 1 607 h de référence, proratisées par la quotité (QuotaModel::REFERENCE_MINUTES).
  • Les 1 607 h sont un volume NET de travail effectif : 365 − 104 (week-ends) − 25 (congés payés) − 8 (jours fériés) = 228 j × 7 h = 1 596 h, arrondies à 1 600 h, plus les 7 h de la journée de solidarité.

Congés payés et jours fériés sont donc DÉJÀ retranchés. Leur imputer des heures de quota les décompterait une seconde fois. Une tentative de décompter un forfait de 7 h par jour de CP a été introduite puis annulée pour cette raison — ne pas la réintroduire.

9.2 Les objets

  • Un planning = un agent × une semaine ISO × un lieu, statut BROUILLON ou VALIDE.
  • Il contient des créneaux (creneau_horaire) porteurs d'un motif (motif_planning). Le flag compte_dans_quota du motif décide de l'imputation : CONGE, AUTRE_ABSENCE et JNT ne comptent pas.
  • Une semaine type (modele_semaine_agent / modele_semaine_creneau) appartient à un agent et décrit un lundi→vendredi générique. Ce n'est pas un planning : rien n'est planifié tant qu'elle n'est pas appliquée à une semaine ou dupliquée sur une période. Elle se compose soit à partir d'une semaine réellement planifiée (saveFromWeek()), soit directement dans l'éditeur (saveFromEntries()) — les deux passent par persistTemplate().
  • Un agent peut être planifié sur plusieurs lieux la même semaine ; id_structure sur l'agent n'est qu'un rattachement par défaut. D'où les contrôles de chevauchement inter-structures.
  • Les vacances scolaires (periode_vacance) déterminent le contexte périscolaire / extrascolaire ; elles sont importées depuis l'API publique data.education.gouv.fr via SchoolHolidayApiService (le conteneur php a donc besoin d'un accès sortant).

9.3 Dates de contrat (V7.15)

Un agent n'est planifiable qu'entre ses dates de contrat (date_debut_contrat / date_fin_contrat). La règle vaut pour tous les motifs, congés compris : hors contrat, il n'y a rien à décompter. Une borne absente ouvre — pas de date de fin signifie CDI.

Elle est appliquée :

  • à l'écriture — assertDatesWithinContract() dans PlanningController et WeekTemplateController ;
  • au contrôle de validation — code HORS_CONTRAT ;
  • à l'inverse, sur la fiche agent : raccourcir un contrat sous des affectations existantes est refusé.

Toute nouvelle voie d'écriture de créneaux doit poser la même garde.

9.4 Comptes d'accès et listes d'affectation (V7.29)

Un compte d'accès coupé retire l'agent des listes d'AFFECTATION : sélecteur de la saisie (AgentModel::listForLocation()) et composition des équipes (allSelectable()).

Un agent sans compte reste proposé — c'est l'état normal avant distribution des jetons, le filtrer viderait les listes d'une installation neuve. Les écrans de consultation et le référentiel « Agents » continuent de le montrer : c'est de là qu'on lui rétablit son accès. allActive() reste la liste non filtrée ; ne pas confondre les deux.

9.5 Les deux modes de planning.php

Deux modes exclusifs : l'éditeur de semaine type (ouvert par défaut) et la saisie semaine par semaine. Le second reste la cible de tous les liens profonds — dès que l'URL porte week ou focus, page-bootstrap.js bascule dessus, sinon un utilisateur venant corriger un planning identifié atterrirait dans l'éditeur de modèles.


10. ⚠️ Piège : logique de validation dupliquée

database/pta_prod_schema_mariadb.sql définit des vues (v_suivi_quota_annuel, v_plannings_en_attente_validation, v_pta_annuel_synthese, v_equipe_pta_controle, v_planning_agent_semaine) et des procédures (sp_controler_planning, sp_valider_planning) — aucune n'est appelée par le code PHP.

Les contrôles réellement appliqués vivent dans PlanningController::validate(), AnnualOverviewController::validateYear() et PtaModel. Modifier une règle métier impose de traiter le PHP ; le SQL correspondant est du patrimoine à ne pas prendre pour la source de vérité.

Forme des contrôles renvoyés au client

{ "niveau": "ERREUR|ALERTE|INFO", "code": "…", "message": "…" }
Niveau Effet Code HTTP
ERREUR Bloque, rien n'est validé 422
ALERTE Demande une confirmation explicite 409, à rappeler avec force_quota: true
INFO Informatif 200

Codes de validation d'un planning :

  • AUCUN_CRENEAU (ERREUR) — planning vide ;
  • CRENEAU_HORS_SEMAINE (ERREUR) — créneau hors des bornes de la semaine ;
  • CHEVAUCHEMENT_INTERNE (ERREUR) — deux créneaux du planning se recouvrent ;
  • HORS_CONTRAT (ERREUR) — affectation hors des dates de contrat ;
  • CHEVAUCHEMENT_AUTRE_STRUCTURE (ERREUR) — recouvrement avec un créneau déjà validé sur un autre lieu. Seuls les plannings validés sont opposables : un brouillon concurrent peut encore être corrigé, le bloquer empêcherait toute validation dans un ordre donné.
  • QUOTA_DEPASSE (ALERTE) — le dépassement est une décision de gestion, pas une anomalie technique.

Tous les contrôles sont collectés avant de trancher, afin que l'utilisateur voie l'ensemble des problèmes d'un coup plutôt qu'un par requête.


11. Les pages

Clé Fichier Titre Rôles typiques
dashboard dashboard.php Tableau de bord Service Enfance (accueil de ce rôle)
planning planning.php Saisie Responsable, Service Enfance
pending pending.php À valider Service Enfance
coverage coverage.php Couverture Responsable, Service Enfance
structures structures.php Par lieu tous sauf DSI
agents agents.php Par agent / Mon planning tous sauf DSI
annual annual.php Vue annuelle Service Enfance
pta pta.php PTA détaillé Service Enfance
teams teams.php Équipes Service Enfance
vacations vacations.php Vacances Service Enfance
manage_agents manage_agents.php Agents Service Enfance
manage_structures manage_structures.php Lieux d'affectation Service Enfance
administration administration.php Administration (couverture + jetons de terrain) Service Enfance
dsi dsi.php Accès & journaux DSI
faq faq.php Guide d'utilisation les trois rôles métier, pas la DSI (hors $navigation)
— role.php Connexion par jeton anonyme
— index.php Aiguillage vers Access::homeUrl() ou role.php —

Groupes de navigation : Piloter (dashboard, pending, coverage) · Planifier (planning, teams) · Consulter (agents, structures, annual, pta) · Management (manage_agents, manage_structures) · Paramétrer (vacations, administration, dsi).

Access::homeUrl() se replie sur planning.php si dashboard n'est pas autorisé (migration non jouée). Ce repli est indispensable : sans lui, requirePage() renverrait indéfiniment vers une page interdite.

Liens profonds

Une page de contrôle peut ouvrir directement le planning concerné :

planning.php?structure_id=2&agent_id=7&week=2026-W31&focus=validation

Paramètres reconnus par page-bootstrap.js : structure_id, agent_id, week, month, year, date, date_debut, date_fin, compte_id, focus, notice.


12. Référence des endpoints API

Conventions communes

  • Verbe : les lectures sont en GET (paramètres en query string), les écritures en POST (corps JSON). Deux endpoints acceptent les deux verbes, signalés ci-dessous.
  • Corps : Content-Type: application/json. Toute charge utile mutante porte csrf_token.
  • Réponse : JSON UTF-8, avec X-Content-Type-Options: nosniff et Cache-Control: no-store (les réponses dépendent du rôle en session : jamais de cache partagé).
  • Endpoints PDF : sortent des octets binaires via PdfResponse::inline(), et leurs erreurs en texte brut avec un code HTTP — jamais en JSON.

Codes de retour

Code Signification
200 Succès
201 Ressource créée
400 Paramètres de requête absents ou mal formés
401 / 403 Session absente, rôle insuffisant, hors périmètre, ou CSRF invalide
404 Ressource introuvable — ou hors périmètre, quand le révéler renseignerait
405 Verbe HTTP non autorisé
409 Conflit : doublon, chevauchement, ou ALERTE demandant confirmation
422 Charge utile invalide, ou ERREUR de validation métier
500 Erreur serveur (détail journalisé, jamais renvoyé sauf PTA_DEBUG=1)
502 Un service externe (BAN, data.education.gouv.fr) n'a pas répondu

Exemple d'appel mutant :

const reponse = await fetch('save_planning.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        structure_id: 2,
        agent_id: 7,
        week: '2026-W31',
        entries: [{ date: '2026-07-27', start: '08:00', end: '12:00', motif_id: 1 }],
        csrf_token: window.PTA_CONFIG.csrfToken,
    }),
});

12.1 Planning — saisie et validation

Endpoint Verbe Contrôleur Garde
api/planning.php GET PlanningController::show requireDraftEditor + requireStructureEdit
save_planning.php POST PlanningController::save requireDraftEditor + requireStructureEdit
api/update_agent_entry.php POST PlanningController::updateAgentEntry requireDraftEditor + requireEntryEdit + requireStructureEdit
api/delete_agent_entry.php POST PlanningController::deleteAgentEntry requireDraftEditor + requireEntryEdit
api/move_or_duplicate_entry.php POST PlanningController::moveOrDuplicateEntry requireDraftEditor + requireEntryEdit
api/copy_agent_week.php POST PlanningController::copyAgentWeek requireService
api/pending_plannings.php GET PlanningController::pendingValidation requireService
validate_planning.php POST PlanningController::validate requireService
reopen_planning.php POST PlanningController::reopen requireDraftEditor + requirePlanningEdit

GET api/planning.php — état complet d'une semaine de saisie. Paramètres : structure_id, agent_id, week (2026-W08). Renvoie le planning et ses créneaux, mais aussi les créneaux de l'agent sur les autres lieux la même semaine (other_entries) : l'éditeur doit les afficher en lecture seule pour rendre les chevauchements visibles avant l'échec.

POST save_planning.php — enregistre le brouillon d'une semaine. Charge utile : structure_id, agent_id, week, entries[], csrf_token. La charge utile remplace intégralement les créneaux de la semaine : une liste entries vide est un cas nominal (« tout supprimer »), pas une erreur. Ordre des contrôles, du moins coûteux au plus coûteux et du plus explicite au plus générique : ① droits et périmètre → ② motifs fermés à l'agent → ③ normalisation ligne à ligne → ④ chevauchements internes → ⑤ chevauchements avec les autres lieux.

POST api/update_agent_entry.php — modifie un créneau depuis la fiche agent. Charge utile : entry_id, structure_id, motif_id, date, start, end, csrf_token. Un créneau peut appartenir à un planning déjà VALIDE : le modèle le repasse alors automatiquement en BROUILLON, d'où le drapeau requires_revalidation dans la réponse.

POST api/delete_agent_entry.php — supprime un créneau depuis la fiche agent. Charge utile : entry_id, csrf_token. Peut également rouvrir un planning validé. Le planning n'est jamais supprimé même s'il devient vide : planning_empty permet à l'interface de le signaler, mais l'enregistrement conserve son historique.

POST api/move_or_duplicate_entry.php — glisser-déposer d'un créneau. Charge utile : entry_id, target_date, action (move | duplicate), csrf_token. duplicate correspond au glisser avec Ctrl. Déposer un créneau sur son jour d'origine n'est pas une erreur : le modèle renvoie l'action none.

POST api/copy_agent_week.php — recopie une semaine entière d'un agent. Charge utile : agent_id, source_week, target_week, mode (merge | replace), csrf_token. Réservé au Service Enfance et non aux responsables : la copie porte sur tous les lieux de l'agent sur la semaine source, elle déborde donc par nature du périmètre d'un responsable.

GET api/pending_plannings.php — file d'attente de validation (alimente pending.php). Les totaux distinguent les minutes saisies (total_minutes) des minutes réellement décomptées du quota (counted_minutes).

POST validate_planning.php — contrôle puis valide un planning. Charge utile : planning_id, force_quota (bool), csrf_token. C'est ici que vivent les règles de validation réellement appliquées — voir §10 pour la liste des codes et les trois issues (422 / 409 / 200).

POST reopen_planning.php — repasse un planning VALIDE en BROUILLON. Charge utile : planning_id, csrf_token. Réouverture volontaire, à distinguer de la réouverture automatique déclenchée par la modification d'un créneau. Le modèle applique un UPDATE conditionné au statut : un retour à zéro ligne signifie qu'un autre utilisateur est passé entre-temps → 409, plutôt qu'un succès trompeur.


12.2 Semaines types

Tous ces endpoints exigent requireService.

Endpoint Verbe Méthode
api/week_templates.php GET WeekTemplateController::index
api/week_template.php GET WeekTemplateController::show
api/save_week_template_entries.php POST WeekTemplateController::storeEntries
api/save_week_template.php POST WeekTemplateController::store
api/apply_week_template.php POST WeekTemplateController::apply
api/preview_week_template_period.php POST WeekTemplateController::previewPeriod
api/apply_week_template_period.php POST WeekTemplateController::applyPeriod
api/delete_week_template.php POST WeekTemplateController::destroy

GET api/week_templates.php — liste des semaines types d'un agent. Paramètre : agent_id. Ne renvoie que des compteurs, pas le contenu : la liste sert à choisir, show() sert à ouvrir.

GET api/week_template.php — contenu détaillé. Paramètres : template_id, agent_id. Le modèle est retrouvé par le couple (template_id, agent_id) et non par le seul identifiant : c'est ce qui empêche de lire le modèle d'un autre agent en changeant l'URL. Les drapeaux structure_active et motif_actif sont conservés bien que toujours vrais à la création : un lieu ou un motif peut être désactivé après l'enregistrement, et l'éditeur doit le signaler plutôt que d'échouer à l'application.

POST api/save_week_template_entries.php — enregistre un modèle composé directement dans l'éditeur. Charge utile : agent_id, name, entries[], replace (bool), csrf_token. Les listes de motifs et de lieux autorisés sont calculées dans le contrôleur puis transmises au modèle : celui-ci ne connaît pas les règles d'accès et se contente de rejeter ce qui n'est pas dans les listes reçues.

POST api/save_week_template.php — capture une semaine planifiée en modèle. Charge utile : agent_id, week, name, replace (bool), csrf_token.

POST api/apply_week_template.php — applique un modèle à une semaine. Charge utile : template_id, agent_id, week, mode (merge | replace), csrf_token. Appliquer sur une semaine validée la repasse en brouillon (requires_revalidation).

POST api/preview_week_template_period.php — simulation sans écriture. Charge utile : template_id, agent_id, start_date, end_date, mode, period_filter, csrf_token. Étape obligatoire dans l'interface avant applyPeriod() : une application sur période touche des dizaines de semaines et peut rouvrir des plannings validés. L'utilisateur doit voir le volume avant de confirmer. period_filter restreint le périmètre au calendrier scolaire : all → toutes les semaines · school → semaines scolaires (périscolaire) · extra → vacances scolaires (extrascolaire).

POST api/apply_week_template_period.php — même charge utile, avec écriture. Le message de retour est volontairement détaillé (créés / ignorés / remplacés / rouverts) : l'opération est massive et difficile à annuler à la main. Les quotas sont renvoyés indexés par date de début de période — une application à cheval sur deux années civiles touche deux quotas annuels distincts, et l'interface doit rafraîchir les deux.

POST api/delete_week_template.php — supprime une semaine type. Charge utile : template_id, agent_id, csrf_token. La suppression est filtrée par agent_id ; un modèle appartenant à un autre agent ressort en 404 plutôt qu'en 403, pour ne pas révéler son existence. Les créneaux déjà appliqués ne sont pas touchés : le modèle est un patron, pas un lien vivant vers les plannings produits.


12.3 Agents

Endpoint Verbe Méthode Garde
api/agents.php GET AgentController::listForPlanning requireDraftEditor + requireStructureEdit
api/agent_overview.php GET AgentController::overview requireAgentView
api/agent_month_overview.php GET AgentController::monthOverview requireAgentView
api/agent_assignments.php GET / POST AgentController::assignments requireService
api/create_agent.php POST AgentController::create requireService
api/update_agent.php POST AgentController::updateProfile requireService

GET api/agents.php — agents sélectionnables dans l'éditeur. Paramètre : structure_id. structure_id à 0 (ou absent) signifie « tous les lieux » : ce n'est licite que pour le Service Enfance, un responsable étant renvoyé en 403 par requireStructureEdit() dès qu'il vise un autre lieu que le sien.

GET api/agent_overview.php — semaine d'un agent, tous lieux confondus. Paramètres : agent_id, week. Vue transverse par agent (et non par lieu comme api/planning.php) : c'est la seule qui donne la charge réelle d'un agent planifié à plusieurs endroits. En lecture seule pour le rôle AGENT.

GET api/agent_month_overview.php — grille mensuelle. Paramètres : agent_id, month (2026-02). La grille est alignée sur les semaines ISO : elle commence au lundi de la semaine du 1er du mois et finit au vendredi de la semaine du dernier jour. Elle déborde donc du mois, et le drapeau in_month de chaque jour permet de griser les débordements. Sans cet alignement, une semaine serait coupée en deux et les cumuls hebdomadaires affichés seraient faux. Les créneaux sont chargés sur toute l'étendue de la grille, débordements compris.

api/agent_assignments.php — rattachement principal d'un agent. Endpoint à double verbe : GET liste les agents (allActive()), POST modifie un rattachement. Charge utile (POST) : agent_id, structure_id (nullable), csrf_token. Un structure_id nul est valide : il détache l'agent de tout lieu par défaut, sans l'empêcher d'être planifié ponctuellement ailleurs.

POST api/create_agent.php — création d'un agent. Charge utile : matricule, nom, prenom, id_poste, type_contrat, structure_id, quotite_travail, date_debut_contrat, date_fin_contrat, email, telephone, adresse, est_diplome, diplome_libelle, droit_enveloppe_formation, commentaire_pta, csrf_token. Les contrôles vont au-delà du « non vide » : poste existant en base, type de contrat dans l'énumération, dates cohérentes entre elles, quotité dans ]0 ; 100], longueurs alignées sur les colonnes SQL (une chaîne trop longue serait sinon tronquée silencieusement par MariaDB), et unicité du matricule (409, pas 422 : c'est un conflit avec l'existant, pas une saisie malformée). La réponse renvoie l'agent complet reconstitué, pour insertion dans la liste sans rechargement.

POST api/update_agent.php — modification de la fiche. Mêmes contrôles que create(), deux champs exceptés, jamais modifiables :

  • le matricule : c'est la clé de rapprochement avec les outils RH ;
  • la quotité de travail : elle détermine le quota annuel déjà consommé, la changer en cours d'année fausserait rétroactivement tous les suivis.

Le nom et le prénom, eux, sont modifiables (V7.13) : mariage, divorce, ou faute de frappe. Un renommage est journalisé avec l'identité précédente. Raccourcir un contrat sous des affectations existantes est refusé (422).


12.4 PDF

Endpoint Méthode Garde Paramètres
api/agent_planning_pdf.php AgentController::pdf requireAgentView agent_id, week, weeks
api/agent_time_summary_pdf.php AgentController::timeSummaryPdf requireAgentView agent_id, date ou year
api/agent_hours_contract_pdf.php AgentController::hoursContractPdf requireAgentView agent_id, date ou year
api/structure_planning_pdf.php StructureController::pdf requireStructureView structure_id, week

Tous en GET. Ils n'appellent pas JsonResponse::bootstrap() et sortent leurs erreurs en texte brut.

api/agent_planning_pdf.php — planning d'un agent sur plusieurs semaines. weeks n'accepte que 6 ou 7 (7 par défaut) : au-delà, la grille ne tient plus sur une page A4 paysage et SimplePdf ne gère pas le report de tableau sur page suivante.

api/agent_time_summary_pdf.php — bilan annuel d'heures, ventilé par motif. La période couverte n'est pas choisie par l'appelant : c'est celle calculée par QuotaModel (année civile 01/01 → 31/12). Le paramètre year n'est qu'un raccourci vers le 1er janvier correspondant.

api/agent_hours_contract_pdf.php — contrat horaire annuel. Document contractuel : il n'affiche que le volume annualisé dû, sans le détail des créneaux planifiés.

api/structure_planning_pdf.php — planning d'un lieu. Reprend volontairement le calcul de StructureController::overview() plutôt que de le factoriser : les deux méthodes doivent rester alignées, toute évolution de l'une appelle la même dans l'autre.


12.5 Lieux d'affectation et couverture

Endpoint Verbe Méthode Garde
api/structure_overview.php GET StructureController::overview requireStructureView
api/create_structure.php POST StructureController::create requireService
api/update_structure.php POST StructureController::updateType requireService
api/coverage_alerts.php GET CoverageController::alerts requireRoles([RESPONSABLE, SERVICE])
api/structure_coverage_rules.php GET CoverageController::rules requireService
api/save_structure_coverage_rules.php POST CoverageController::saveRules requireService

GET api/structure_overview.php — semaine d'un lieu, agent par agent. Paramètres : structure_id, week. La liste d'agents ne se limite pas aux agents rattachés au lieu : elle inclut ceux qui y sont ponctuellement planifiés, distingués par le drapeau lieu_principal. Un agent peut travailler sur plusieurs lieux la même semaine, et l'omettre donnerait une vue fausse de l'effectif présent. Les créneaux sont indexés par agent en PHP (une seule lecture, puis regroupement en mémoire) plutôt que par une requête par agent.

POST api/create_structure.php — création d'un lieu. Charge utile : code, nom, adresse, type_affectation, csrf_token. Le code est normalisé avant contrôle (espaces → underscores, puis majuscules). Il sert d'identifiant lisible (nom de fichier PDF, exports), d'où le format restreint à [A-Z0-9_-]{2,50}. Un doublon ressort en 409. type_affectation (PERISCOLAIRE | EXTRASCOLAIRE) détermine le contexte par défaut du lieu ; il reste croisé avec le calendrier des vacances.

POST api/update_structure.php — change le type d'un lieu. Charge utile : structure_id, type_affectation, csrf_token. Seul le type est modifiable : le code sert de référence stable, et le nom n'est pas exposé à l'édition dans cette version.

GET api/coverage_alerts.php — trous de couverture d'une semaine. Paramètres : week, structure_id (facultatif — sans lui, balaie tous les lieux actifs). Pour un responsable, le structure_id demandé est ignoré et écrasé par son propre lieu : un simple 403 l'empêcherait de consulter son écran depuis une URL partagée. Trois familles de retours, à ne pas confondre :

  • alerts — plages réellement sous-couvertes ;
  • validation_warnings — couverture assurée par des plannings encore en brouillon, donc pas encore fiable ;
  • unconfigured_structures — lieux sans aucune règle. Ils ne peuvent pas produire d'alerte, et les taire les ferait passer pour conformes.

GET api/structure_coverage_rules.php — règles d'un lieu. Paramètre : structure_id.

POST api/save_structure_coverage_rules.php — remplace intégralement les règles d'un lieu. Charge utile : structure_id, rules[], csrf_token. La liste reçue devient l'unique jeu de règles ; une liste vide supprime toute règle (le lieu redevient « non configuré »). Il n'y a pas d'ajout ni de suppression unitaire. Les règles sont validées dans le contrôleur — jour ouvré, horaires au quart d'heure, fin après début, minimum dans [1, 50], puis absence de chevauchement — avant d'être confiées au modèle, qui n'effectue aucun contrôle métier.


12.6 Vue annuelle et PTA

Tous ces endpoints exigent requireService.

Endpoint Verbe Méthode
api/annual_overview.php GET AnnualOverviewController::overview
api/annual_validate.php POST AnnualOverviewController::validateYear
api/pta_summary.php GET PtaController::summary
api/pta_constraint.php POST PtaController::constraint
api/pta_wish.php POST PtaController::wish

GET api/annual_overview.php — répartition de l'année d'un agent. Paramètres : agent_id, year (facultatif, borné à [2000, 2100], retombe sur l'année en cours).

POST api/annual_validate.php — contrôle puis validation de toute l'année en une opération. Charge utile : agent_id, year, force_quota (bool), csrf_token. Aucune validation partielle : si une erreur est trouvée, aucun planning n'est validé. Les semaines concernées sont retournées au front afin de pouvoir les ouvrir et les corriger. Un succès partiel laisserait l'utilisateur devant une année à moitié validée, sans moyen simple de savoir ce qui reste. Trois issues, alignées sur PlanningController::validate() :

  • 422 + status: "errors" — au moins une ERREUR, rien n'est validé ;
  • 409 + status: "warning" — quota annuel dépassé, rappeler avec force_quota: true ;
  • 200 + status: "validated" ou "nothing_to_validate".

Contrairement à validate() unitaire, le quota est ici évalué à l'échelle de l'année (annualForApi) et non de la semaine.

GET api/pta_summary.php — synthèse du PTA d'un agent à une date donnée. Paramètres : agent_id, date (facultative, AAAA-MM-JJ, défaut aujourd'hui). La date sert de point d'observation : elle détermine la période de quota retenue et les contraintes en vigueur ce jour-là.

POST api/pta_constraint.php — enregistre ou supprime une contrainte prioritaire. Charge utile : action (SAVE | DELETE), agent_id, csrf_token, puis selon l'action : constraint_id, ou type_contrainte / date_debut / date_fin / commentaire / quotite_temporaire / maximum_heures_jour / maximum_heures_semaine / interdit_matin / interdit_midi / interdit_soir. Deux types seulement, volontairement fermés : MEDICALE et TEMPS_PARTIEL_THERAPEUTIQUE. Une date de fin vide signifie « sans terme connu ». Le commentaire est obligatoire : une contrainte restreint la planification d'un agent, elle doit rester justifiable a posteriori. ⚠️ Les maximums arrivent en heures depuis le formulaire et sont convertis en minutes pour la base, unité de tous les cumuls de l'application.

POST api/pta_wish.php — enregistre ou supprime un souhait de lieu. Charge utile : action (SAVE | DELETE), agent_id, csrf_token, puis wish_id, ou structure_id / type_souhait / priorite / distance_km / commentaire. Deux types : PREFERENCE et INTERDICTION. La priorité (1 à 5) ordonne plusieurs préférences entre elles ; la distance en kilomètres sert à arbitrer les affectations éloignées.


12.7 Équipes

Tous ces endpoints exigent requireService.

api/teams.php — endpoint à double verbe. GET : liste, avec date_debut / date_fin facultatifs pour ne montrer que les équipes actives sur une période. POST (TeamController::index) : création. Charge utile : structure_id, type_equipe, libelle, date_debut, date_fin, nombre_enfants_moins_6, nombre_enfants_6_plus, ratio_moins_6, ratio_6_plus, minimum_agents, pourcentage_diplomes_min, csrf_token. Les valeurs numériques sont bornées par max() plutôt que rejetées : un effectif négatif n'a pas de sens mais ne doit pas bloquer la création, et un ratio à zéro provoquerait une division par zéro au calcul du taux d'encadrement.

POST api/team_members.php (TeamController::member) — ajoute ou retire un agent. Charge utile : action (ADD par défaut | REMOVE), team_id, agent_id, fonction_equipe, commentaire, csrf_token. Trois fonctions : RESPONSABLE, ANIMATION, RESTAURATION_ENTRETIEN — elles conditionnent le calcul du taux d'encadrement. L'exception d'ajout ressort en 422 avec son propre message : le modèle y place les refus métier lisibles (agent déjà membre, par exemple), destinés à l'utilisateur.

POST api/team_status.php (TeamController::status) — fait avancer une équipe dans son cycle de vie. Charge utile : team_id, statut (BROUILLON | A_CONTROLER | VALIDEE), csrf_token. Aucune transition n'est imposée : tout statut valide est acceptable depuis n'importe quel autre, y compris un retour en arrière. Le suivi est indicatif, pas un verrou.


12.8 Vacances scolaires

Tous ces endpoints exigent requireService.

GET api/vacation_periods.php (VacationController::index) — périodes enregistrées. Paramètre : school_year (facultatif, 2026-2027).

GET api/preview_school_holidays.php (VacationController::preview) — propositions de l'API officielle, sans aucune écriture en base. Paramètres : academy, year (année civile). Le 502 (et non 500) en cas d'échec dit ce qui s'est passé : c'est le service distant qui n'a pas répondu, pas PTA qui a échoué. Le conteneur php a besoin d'un accès réseau sortant. Les warnings signalent les périodes que le service n'a pas su interpréter ; elles sont montrées à l'utilisateur plutôt que tues.

POST api/save_vacation_periods.php (VacationController::save) — enregistre les périodes retenues. Charge utile : periods[], csrf_token. Plafond de 50 périodes par envoi : large excédent d'une année scolaire complète, il borne le coût d'un envoi malformé ou répété. Les libellés, académies et zones sont tronqués avec les fonctions mb_* aux longueurs des colonnes — une troncature en octets couperait un caractère accentué en deux et produirait de l'UTF-8 invalide en base. source distingue MANUEL de DATA_GOUV ; une valeur inconnue retombe sur MANUEL au lieu de faire échouer l'enregistrement.

POST api/delete_vacation_period.php (VacationController::delete) — supprime une période. Charge utile : period_id, csrf_token. Aucun contrôle d'usage : supprimer une période change le contexte scolaire/extrascolaire des journées concernées, mais ne touche aucun créneau déjà planifié.


12.9 Tableau de bord et recherche d'adresse

GET api/dashboard_summary.php (DashboardController::summary, requireService). Paramètres : scope, week, year (tous facultatifs). Deux portées, à ne pas confondre :

  • scope=badges — uniquement le compteur de plannings en attente. Cette variante est appelée depuis toutes les pages pour alimenter la pastille de navigation : elle doit rester à une seule requête SQL ;
  • portée complète (paramètre absent) — les quatre blocs de l'écran, soit plusieurs dizaines de requêtes, acceptable uniquement sur le tableau de bord lui-même.

GET api/search_address.php (AddressController::search, requireService) — propositions d'adresses via la Base Adresse Nationale. Paramètres : q (saisie en cours), limit (facultatif, 1 à 8). Le garde est celui de l'écriture des fiches agent : c'est le seul rôle qui saisit une adresse.


12.10 Comptes d'accès délégués — Service Enfance (V7.27)

Tous ces endpoints exigent requireService et vérifient que le compte visé relève des ROLES_DELEGUES (AGENT, RESPONSABLE_STRUCTURE). Un compte hors périmètre ressort en 403 et le refus est journalisé (COMPTE_HORS_PERIMETRE / ROLE_HORS_PERIMETRE).

GET api/acces_comptes.php (comptes) — comptes de terrain et identités sans compte. Paramètres : role (dans la liste déléguée), actif, q.

POST api/acces_compte_creer.php (creerCompte) — crée un compte de terrain et renvoie son jeton en clair. Charge utile : role, agent_id, structure_id, commentaire, csrf_token. Le libellé n'est pas saisi : pour ces deux rôles il est toujours déduit de l'identité (nom de l'agent, nom du lieu) par CompteModel. Un titulaire ne peut donc pas être maquillé sous un autre nom dans la liste.

POST api/acces_compte_jeton.php (regenererJeton) — régénère le jeton d'un compte de terrain. Charge utile : compte_id, csrf_token. C'est la procédure du jeton perdu comme du jeton compromis : l'ancien cesse immédiatement de fonctionner.

POST api/acces_compte_statut.php (changerStatutCompte) — active, désactive ou supprime. Charge utile : compte_id, action (ACTIVER | DESACTIVER | SUPPRIMER), csrf_token. La suppression efface le compte mais pas ses traces : le journal conserve le libellé figé de l'acteur.

POST api/acces_comptes_manquants.php (genererJetonsManquants) — équipe d'un coup toutes les identités actives sans compte. Charge utile : csrf_token. Sans danger d'élargissement : v_acces_compte_manquant ne connaît que des agents et des lieux, jamais un compte privilégié. La réponse porte tous les jetons produits, en clair — seule occasion de les lire.


12.11 Espace DSI — comptes privilégiés et journal (V7.11)

Tous ces endpoints exigent requireDsi.

GET api/dsi_comptes.php (comptes) — comptes d'accès et identités sans compte. Paramètres : role, actif, q.

POST api/dsi_compte_creer.php (creerCompte) — crée un compte, tous rôles, et renvoie son jeton. Charge utile : role, agent_id, structure_id, libelle, commentaire, csrf_token.

POST api/dsi_compte_jeton.php (regenererJeton) — régénère un jeton. Charge utile : compte_id, csrf_token.

POST api/dsi_compte_mot_de_passe.php (reinitialiserMotDePasse) — efface le mot de passe d'un compte privilégié. Charge utile : compte_id, csrf_token. La DSI ne remplace pas le mot de passe, elle le retire : le titulaire en choisira un nouveau à sa prochaine connexion, avec son jeton. C'est ce qui garantit que personne d'autre que lui ne l'a jamais connu — y compris la personne qui vient de le réinitialiser.

POST api/dsi_compte_statut.php (changerStatutCompte) — active, désactive ou supprime. Charge utile : compte_id, action, csrf_token. Refuse (422) de désactiver ou supprimer le compte avec lequel on est connecté. La suppression conserve les traces du journal (clé étrangère en ON DELETE SET NULL).

POST api/dsi_comptes_manquants.php (genererJetonsManquants) — outil de mise en service : à l'installation, aucun agent ni responsable n'a de jeton, donc personne ne peut entrer. Charge utile : csrf_token. La réponse porte tous les jetons produits en clair — seule occasion de les lire, faite pour être imprimée ou copiée avant de refermer l'écran.

GET api/dsi_journal.php (journal) — recherche paginée dans le journal. Paramètres : categorie, resultat, role, id_compte, q, du, au, page, taille.

POST api/dsi_journal_purge.php (purgerJournal) — supprime les entrées les plus anciennes. Charge utile : jours (minimum 30), csrf_token. La purge est elle-même journalisée, et l'est après coup : sa propre trace ne peut donc pas être emportée par la purge qu'elle décrit.


13. Base de données

Tables du socle métier

structure · poste_agent · agent · agent_structure · tranche_age · structure_capacite · structure_couverture_horaire · structure_creneau_reference · motif_planning · quota_agent_annuel · pta_annuel · semaine · planning · creneau_horaire · modele_semaine_agent · modele_semaine_creneau · periode_vacance · agent_structure_souhait · agent_contrainte · equipe_pta · equipe_pta_membre

Tables ajoutées par migration

compte_acces (V7.11) · journal_activite (V7.11) · acces_role_page (V7.8) · acces_role_permission (V7.8) · schema_migration (registre du script de migration)

Migrations, dans l'ordre d'application

Fichier Apport
migration_v7_8_securite_acces.sql Matrice des droits en vues SQL (V7.8)
migration_v7_9_tableau_de_bord.sql Page dashboard (V7.9)
migration_v7_9b_droit_enveloppe_formation.sql Colonne droit_enveloppe_formation
migration_v7_9c_retrait_formation_repartition.sql Retrait de la formation de la répartition
migration_v7_9d_faq.sql Page faq pour les trois rôles
migration_v7_9e_dsi_comptes_journal.sql Rôle DSI, compte_acces, journal_activite (V7.11)
migration_v7_9f_mot_de_passe_admin.sql Second facteur des comptes privilégiés (V7.12)
migration_v7_9g_management.sql Pages manage_agents / manage_structures (V7.14)
migration_v7_9h_jetons_service_enfance.sql can_manage_field_access (V7.27)

Rappel : ordre alphabétique, pas ordre de version. Nommer une nouvelle migration en suivant le préfixe de la dernière en date (migration_v7_9i_…), jamais le numéro de version fonctionnelle.


14. Où chercher quoi

Besoin Fichier
Règles de validation d'un planning app/Controllers/PlanningController::validate()
Validation annuelle app/Controllers/AnnualOverviewController::validateYear()
Calcul du quota app/Models/QuotaModel.php
Contrôle des rôles et périmètres app/Core/Access.php (+ vues SQL de la V7.8)
Authentification, jetons, mots de passe app/Controllers/RoleController.php, app/Models/CompteModel.php
Traces d'activité app/Core/Journal.php, app/Models/JournalModel.php
En-têtes de sécurité et CSP app/Core/Security.php
Génération PDF app/Services/SimplePdf.php + les trois services *PdfService.php
Import des vacances app/Services/SchoolHolidayApiService.php
Recherche d'adresse app/Services/AddressApiService.php
Contrôle de schéma au démarrage app/Models/SchemaModel.php
Navigation et groupes d'onglets app/Views/layouts/header.php
Injection des globales JS app/Views/layouts/footer.php
Amorçage des modules JS assets/js/page-bootstrap.js
Refus d'accès nginx docker/nginx/default.conf
Application des migrations docker/db/apply-migrations.sh

15. Checklist avant commit

  • find . -name '*.php' -not -path './.git/*' -exec php -l {} \; passe.
  • Tout endpoint mutant a : un garde Access, Request::requireMethod('POST'), Csrf::assertPayload(), un Journal::ecriture() de succès.
  • Toute nouvelle voie d'écriture de créneaux appelle assertDatesWithinContract().
  • Nouvelle table ou colonne obligatoire → listes de SchemaModel mises à jour.
  • Nouvelle migration → idempotente, et nommée selon l'ordre alphabétique voulu.
  • Nouvelle page → $navigation, $navigationGroups, acces_role_page (migration SQL).
  • JS/CSS modifié → ?v= bumpé dans header.php, footer.php et pages/role.php.
  • Aucun onclick= ni style= inline ; tout <script> inline porte le nonce.
  • Échappement côté JS via PTA.utils.escapeHtml, jamais une variante locale.
  • docker compose up -d --build php nginx pour voir l'effet du changement.

16. Historique des versions

Chaque fichier documente une version successive et explique pourquoi une décision a été prise. Ils restent utiles pour comprendre un choix, mais seul le code fait foi.

Fichier Sujet
README_V6.md Passage de la SPA à l'architecture multipage
README_V7.md Socle PTA annuel, équipes mercredis/vacances, contrôles métier
README_V7_1.md → README_V7_7.md Vue annuelle, rôles et droits, ajustements successifs
README_V7_8.md Sécurité et matrice des droits déplacée en vues SQL
README_V7_9.md Tableau de bord du Service Enfance
README_V7_10.md Éditeur de semaine type, application sur période
README_V7_11.md Connexion par jeton, espace DSI, journal d'activité
README_V7_12.md Mot de passe des comptes privilégiés
README_V7_13.md Nom et prénom modifiables sur la fiche agent
README_V7_14.md Groupe de navigation « Management »
README_V7_15.md Affectations bornées par les dates de contrat
README_V7_27.md Distribution des jetons de terrain par le Service Enfance