66 KiB
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_*.mdde 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
- Ce que fait l'application
- Contraintes techniques structurantes
- Démarrage
- Architecture
- Rôles, authentification et droits
- Journalisation
- Sécurité — contraintes à respecter
- JavaScript
- Modèle métier
- Piège : logique de validation dupliquée
- Les pages
- 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
- Base de données
- Où chercher quoi
- Checklist avant commit
- 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
- Se connecter avec le jeton DSI d'installation
PTA-INSTALLATION-DSI, écrit en clair dansdatabase/migration_v7_9e_dsi_comptes_journal.sql— il est donc public. - Le régénérer immédiatement depuis la page « Accès & journaux ». L'écran alerte tant que ce n'est pas fait.
- Générer les jetons manquants pour équiper les agents et les responsables de lieu.
- 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 avantmigration_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_PORTetPMA_PORTdu 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_PORTde.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
- Créer le fichier mince dans
api/(voir modèle ci-dessus). - 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
- Fichier PHP à la racine (
require page_bootstrap.php+runPage('cle')). - Méthode dans
PageController— son nom EST la clé de page utilisée paracces_role_page(d'oùmanage_agents()en snake_case, à rebours du reste du projet). - Vue
app/Views/pages/cle.php+ partielapp/Views/partials/cle.php. - Module JS, déclaré dans les tableaux
scripts/modulesde la méthode. - Inscription de la clé dans
$navigationet dans un groupe de$navigationGroups(app/Views/layouts/header.php). - 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 :
- 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 unCsrf::assertPayload(). - Les lectures ne sont jamais journalisées (choix de volume). Les refus le sont, et
AccesscommeCsrfs'en chargent déjà — inutile de les doubler dans les contrôleurs. - Le journal ne doit jamais faire échouer l'action :
Journalavale 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-…', sansunsafe-inline. Tout<script>inline ajouté à une vue doit porternonce="<?= $cspNonce ?>". Aucunonclick=nistyle=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), lesREADME*et les extensions.env .sql .ini .log .bak .dump .yml .yaml .md. Ces règles doivent suivre toute réorganisation de l'arborescence. PTA_DEBUGreste à0. Les détails d'exception passent parSecurity::reportThrowable(), qui journalise côté serveur et ne renvoie rien au client.session_start()ne doit jamais être appelé directement : passer parApp\Core\Session::start().- Ne jamais redéfinir
PMA_USER/PMA_PASSWORDdans 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 :
app/Views/layouts/header.phpapp/Views/layouts/footer.phpapp/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
BROUILLONouVALIDE. - Il contient des créneaux (
creneau_horaire) porteurs d'un motif (motif_planning). Le flagcompte_dans_quotadu motif décide de l'imputation :CONGE,AUTRE_ABSENCEetJNTne 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 parpersistTemplate(). - Un agent peut être planifié sur plusieurs lieux la même semaine ;
id_structuresur 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 viaSchoolHolidayApiService(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()dansPlanningControlleretWeekTemplateController; - 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 surplanning.phpsidashboardn'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 enPOST(corps JSON). Deux endpoints acceptent les deux verbes, signalés ci-dessous. - Corps :
Content-Type: application/json. Toute charge utile mutante portecsrf_token. - Réponse : JSON
UTF-8, avecX-Content-Type-Options: nosniffetCache-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 avecforce_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(), unJournal::ecriture()de succès. - Toute nouvelle voie d'écriture de créneaux appelle
assertDatesWithinContract(). - Nouvelle table ou colonne obligatoire → listes de
SchemaModelmises à 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é dansheader.php,footer.phpetpages/role.php. - Aucun
onclick=nistyle=inline ; tout<script>inline porte lenonce. - Échappement côté JS via
PTA.utils.escapeHtml, jamais une variante locale. docker compose up -d --build php nginxpour 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 |