1252 lines
66 KiB
Markdown
1252 lines
66 KiB
Markdown
# 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](#1-ce-que-fait-lapplication)
|
||
2. [Contraintes techniques structurantes](#2-contraintes-techniques-structurantes)
|
||
3. [Démarrage](#3-démarrage)
|
||
4. [Architecture](#4-architecture)
|
||
5. [Rôles, authentification et droits](#5-rôles-authentification-et-droits)
|
||
6. [Journalisation](#6-journalisation)
|
||
7. [Sécurité — contraintes à respecter](#7-sécurité--contraintes-à-respecter)
|
||
8. [JavaScript](#8-javascript)
|
||
9. [Modèle métier](#9-modèle-métier)
|
||
10. [Piège : logique de validation dupliquée](#10-️-piège--logique-de-validation-dupliquée)
|
||
11. [Les pages](#11-les-pages)
|
||
12. [**Référence des endpoints API**](#12-référence-des-endpoints-api)
|
||
· [Planning](#121-planning--saisie-et-validation)
|
||
· [Semaines types](#122-semaines-types)
|
||
· [Agents](#123-agents)
|
||
· [PDF](#124-pdf)
|
||
· [Lieux & couverture](#125-lieux-daffectation-et-couverture)
|
||
· [Annuel & PTA](#126-vue-annuelle-et-pta)
|
||
· [Équipes](#127-équipes)
|
||
· [Vacances](#128-vacances-scolaires)
|
||
· [Tableau de bord](#129-tableau-de-bord-et-recherche-dadresse)
|
||
· [Comptes délégués](#1210-comptes-daccès-délégués--service-enfance-v727)
|
||
· [Espace DSI](#1211-espace-dsi--comptes-privilégiés-et-journal-v711)
|
||
13. [Base de données](#13-base-de-données)
|
||
14. [Où chercher quoi](#14-où-chercher-quoi)
|
||
15. [Checklist avant commit](#15-checklist-avant-commit)
|
||
16. [Historique des versions](#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
|
||
|
||
```bash
|
||
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` :
|
||
|
||
```bash
|
||
docker compose up -d --build php nginx
|
||
```
|
||
|
||
### Commandes utiles
|
||
|
||
```bash
|
||
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 :
|
||
|
||
```php
|
||
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 :
|
||
|
||
```php
|
||
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.
|
||
|
||
```js
|
||
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 :
|
||
|
||
```php
|
||
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 :
|
||
|
||
```js
|
||
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
|
||
|
||
```json
|
||
{ "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 :
|
||
|
||
```js
|
||
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 |
|