update
This commit is contained in:
@@ -74,3 +74,41 @@ automatiquement au déploiement par le service `db-migrate` du docker-compose.
|
||||
## V7.4 — Rôles et droits d’accès
|
||||
|
||||
La première arrivée ouvre `role.php`. Les droits Agent, Responsable de structure et Service Enfance sont vérifiés dans les pages et les contrôleurs API. Voir `README_V7_4.md`.
|
||||
|
||||
## V7.11 — Connexion par jeton, espace DSI et journal d’activité
|
||||
|
||||
`role.php` n’est plus un sélecteur de rôle : chaque utilisateur se connecte avec un jeton personnel
|
||||
distribué par la DSI, dont la base ne conserve qu’une empreinte SHA-256. Un quatrième rôle `DSI`
|
||||
ouvre `dsi.php` : gestion des comptes d’accès (dont les administrateurs Service Enfance) et lecture
|
||||
du journal d’activité, qui trace les connexions, les écritures en base et les refus d’accès. La
|
||||
migration `database/migration_v7_9e_dsi_comptes_journal.sql` est obligatoire ; elle crée aussi le
|
||||
compte DSI d’installation dont le jeton, public, est à régénérer immédiatement. Voir
|
||||
`README_V7_11.md`.
|
||||
|
||||
## V7.12 — Mot de passe des comptes privilégiés
|
||||
|
||||
Les comptes Service Enfance et DSI ajoutent un second facteur à leur jeton : un mot de passe choisi
|
||||
par leur titulaire à sa première connexion, que la DSI ne peut qu’effacer, jamais lire. Les agents et
|
||||
les responsables de lieu restent à un seul facteur. Migration
|
||||
`database/migration_v7_9f_mot_de_passe_admin.sql`. Voir `README_V7_12.md`.
|
||||
|
||||
## V7.13 — Nom et prénom modifiables
|
||||
|
||||
La fiche agent laisse désormais corriger le nom et le prénom, à côté des autres champs déjà
|
||||
modifiables. Le matricule et la quotité de travail restent immuables. Un renommage est journalisé
|
||||
avec l’identité précédente. Aucune migration. Voir `README_V7_13.md`.
|
||||
|
||||
## V7.14 — Groupe de navigation « Management »
|
||||
|
||||
Les agents et les lieux d’affectation quittent l’onglet « Administration » pour deux onglets
|
||||
distincts, réunis dans un groupe « Management » de la barre de navigation, au même titre que
|
||||
« Piloter » ou « Planifier ». « Administration » ne conserve que les horaires de couverture.
|
||||
Migration `database/migration_v7_9g_management.sql` (droits d’accès aux deux pages). Voir
|
||||
`README_V7_14.md`.
|
||||
|
||||
## V7.15 — Affectations bornées par le contrat
|
||||
|
||||
Un agent ne peut plus être planifié avant sa date de début de contrat ni après sa date de fin, quel
|
||||
que soit le chemin d’écriture (saisie, déplacement, copie de semaine, semaine type). La validation
|
||||
signale les affectations devenues hors contrat, et raccourcir un contrat sous des affectations
|
||||
existantes est refusé. Aucune migration. Voir `README_V7_15.md`.
|
||||
|
||||
165
docs/README_V7_11.md
Normal file
165
docs/README_V7_11.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# PTA V7.11 — Connexion par jeton, espace DSI et journal d'activité
|
||||
|
||||
## Le problème traité
|
||||
|
||||
Depuis l'origine, `role.php` était un **sélecteur, pas une authentification** : n'importe qui
|
||||
ouvrant l'application choisissait librement son rôle et son identité, y compris « Service Enfance »,
|
||||
qui valide les plannings de toute la collectivité. Les READMEs successifs et `CLAUDE.md` la
|
||||
désignaient comme *la faille ouverte la plus importante du projet*.
|
||||
|
||||
Deux conséquences, au-delà du contrôle d'accès lui-même :
|
||||
|
||||
- **aucune action n'était imputable** — un planning validé, un agent modifié, un lieu créé ne
|
||||
portaient le nom de personne ;
|
||||
- **rien n'était traçable** : pas de journal, donc aucune façon de répondre à « qui a rouvert cette
|
||||
semaine ? » ou « depuis quand cet accès est-il utilisé ? ».
|
||||
|
||||
## Ce qui change
|
||||
|
||||
### 1. On entre avec un jeton, plus avec un choix
|
||||
|
||||
Chaque utilisateur — agent, responsable de lieu, administrateur du Service Enfance, DSI — reçoit un
|
||||
**jeton personnel** de la forme `PTA-XXXXXX-XXXXXX-XXXXXX-XXXXXX`, tiré au sort par le serveur sur
|
||||
120 bits d'entropie, dans un alphabet sans caractères ambigus (ni `I`, ni `O`, ni `0`, ni `1`).
|
||||
|
||||
Il n'y a **aucun mot de passe** dans PTA : rien à choisir, rien à renouveler périodiquement, aucune
|
||||
procédure de réinitialisation à écrire. Un jeton perdu ne se retrouve pas, il se **régénère**, ce qui
|
||||
révoque l'ancien à la seconde.
|
||||
|
||||
Deux façons de le présenter, l'une et l'autre acceptées :
|
||||
|
||||
- le **champ de saisie** de `role.php` (POST) — le jeton ne transite alors ni par l'URL, ni par
|
||||
l'historique du navigateur, ni par les journaux de nginx ;
|
||||
- le **lien direct** `role.php?token=…` — plus commode à envoyer par courriel, mais le jeton
|
||||
s'inscrit dans l'historique et les journaux du serveur web, et un lien transféré donne l'accès.
|
||||
La session ouverte, une redirection immédiate retire le jeton de la barre d'adresse.
|
||||
|
||||
Le rôle, l'agent et le lieu ne sont plus déclarés par l'utilisateur : ils sont **lus sur son compte**.
|
||||
Le reste de la mécanique de droits n'a pas bougé d'une ligne — `Access` interroge les mêmes vues SQL
|
||||
qu'en V7.8.
|
||||
|
||||
### 2. La base ne conserve jamais le jeton
|
||||
|
||||
`compte_acces` ne stocke qu'une **empreinte SHA-256** du jeton et ses onze premiers caractères, qui
|
||||
servent uniquement à reconnaître une ligne dans la liste. Un vol du dump SQL ou un accès à phpMyAdmin
|
||||
ne donne donc **aucun accès applicatif**.
|
||||
|
||||
Conséquence assumée, visible partout dans l'interface : **un jeton n'est affiché qu'une seule fois**,
|
||||
dans le panneau « Jetons à transmettre » qui suit sa création. Il faut le copier à ce moment-là.
|
||||
|
||||
L'empreinte n'est volontairement pas salée, contrairement à ce qu'on ferait d'un mot de passe : un
|
||||
jeton est tiré par le serveur, pas devinable par dictionnaire, et l'absence de sel est ce qui permet
|
||||
de retrouver le compte en **une lecture d'index** à la connexion plutôt qu'en comparant à chaque
|
||||
compte.
|
||||
|
||||
### 3. Un quatrième rôle : DSI
|
||||
|
||||
`DSI` rejoint `AGENT`, `RESPONSABLE_STRUCTURE` et `SERVICE_ENFANCE` dans `acces_role_page` et
|
||||
`acces_role_permission`. C'est une administration **technique**, pas fonctionnelle : la DSI ne voit
|
||||
ni planning, ni quota, ni agent — une seule page lui est ouverte, `dsi.php`, qui contient :
|
||||
|
||||
- **les comptes d'accès** : création, régénération de jeton, désactivation, suppression, filtres par
|
||||
type, par état et par nom ;
|
||||
- **« Générer les jetons manquants »** : équipe d'un coup tous les agents et tous les lieux actifs
|
||||
qui n'ont pas encore de compte. C'est l'outil de mise en service — sans lui, personne ne pourrait
|
||||
entrer le jour du déploiement ;
|
||||
- **le journal d'activité** : recherche filtrée et paginée, quatre chiffres de tête, purge des
|
||||
entrées anciennes.
|
||||
|
||||
Deux permissions nouvelles, `can_manage_access` et `can_read_journal`, sont accordées à la seule DSI
|
||||
et **refusées explicitement** aux trois rôles métier.
|
||||
|
||||
### 4. Ce qui est journalisé — et ce qui ne l'est pas
|
||||
|
||||
`journal_activite` est alimenté par `App\Core\Journal`, jamais par des déclencheurs SQL : la base ne
|
||||
sait pas qui agit, la connexion MariaDB étant la même pour tout le monde.
|
||||
|
||||
| Catégorie | Contenu |
|
||||
| --- | --- |
|
||||
| `CONNEXION` | Connexion réussie, jeton refusé, blocage anti-force-brute, déconnexion |
|
||||
| `ECRITURE` | Toute modification en base, avec la table et l'identifiant visés |
|
||||
| `ACCES_REFUSE` | Page interdite, rôle insuffisant, périmètre dépassé, jeton CSRF invalide |
|
||||
| `ADMINISTRATION` | Actions de la DSI sur les comptes et le journal, purge comprise |
|
||||
| `SYSTEME` | Réservé aux incidents techniques |
|
||||
|
||||
**Les lectures ne sont pas tracées** : elles représenteraient l'essentiel du volume pour une valeur
|
||||
d'audit très faible.
|
||||
|
||||
Deux garanties tenues par le code :
|
||||
|
||||
- écrire dans le journal **ne peut jamais faire échouer l'action journalisée** — toute erreur
|
||||
d'insertion est avalée et renvoyée dans `error_log` ;
|
||||
- la trace est **figée** : rôle et libellé de l'acteur sont copiés au moment de l'action, et la clé
|
||||
étrangère vers `compte_acces` est en `ON DELETE SET NULL`. Supprimer un compte n'efface pas son
|
||||
histoire, et renommer un agent ne la réécrit pas.
|
||||
|
||||
### 5. Freins et garde-fous
|
||||
|
||||
- **Anti-force-brute** : au-delà de 10 jetons refusés en 15 minutes depuis une même adresse IP, plus
|
||||
aucun jeton n'est même comparé, et le blocage est journalisé. Le compteur, c'est le journal
|
||||
lui-même — aucune table supplémentaire.
|
||||
- **Révocation immédiate** : `Access` relit le compte à **chaque requête**. Désactiver un compte,
|
||||
désactiver l'agent qui le porte ou fermer son lieu referme la session en cours à la requête
|
||||
suivante, sans attendre son expiration.
|
||||
- **On ne se coupe pas la branche** : la DSI ne peut ni désactiver ni supprimer le compte avec lequel
|
||||
elle est connectée, et le modèle refuse de retirer le dernier accès DSI actif.
|
||||
- **Purge plancher à 30 jours** : une purge plus courte reviendrait à effacer les traces d'un
|
||||
incident avant même qu'il soit constaté.
|
||||
|
||||
## Mise en service
|
||||
|
||||
1. Jouer les migrations (`docker compose up -d --build` s'en charge) ; `migration_v7_9e_…` crée les
|
||||
tables, les vues, le rôle DSI et **un compte DSI d'installation**.
|
||||
2. Se connecter avec le **jeton d'installation**, écrit en clair dans la migration :
|
||||
|
||||
```
|
||||
PTA-INSTALLATION-DSI
|
||||
```
|
||||
|
||||
Il est **public** — il est versionné dans le dépôt. La page DSI affiche un bandeau rouge tant
|
||||
qu'il n'a pas été remplacé.
|
||||
3. **Régénérer immédiatement ce jeton** (bouton « Régénérer » sur la ligne du compte DSI), le copier
|
||||
et le conserver. Créer au passage un second compte DSI si plusieurs personnes doivent administrer.
|
||||
4. Cliquer sur **« Générer les jetons manquants »** : un jeton par agent et par lieu actif. Le
|
||||
panneau les affiche tous, une seule fois — les copier avant de le refermer, puis les distribuer.
|
||||
5. Créer les comptes **Service Enfance** un par un (ils ne correspondent à aucune identité en base,
|
||||
leur libellé est saisi à la main).
|
||||
|
||||
### Perte du jeton DSI
|
||||
|
||||
C'est le seul cas qui ne se résout pas dans l'interface. Procédure de secours, en SQL :
|
||||
|
||||
```sql
|
||||
DELETE FROM compte_acces WHERE id_compte = 1;
|
||||
```
|
||||
|
||||
puis rejouer la migration (`docker compose run --rm db-migrate` après avoir touché le fichier, ou
|
||||
directement l'`INSERT` de la section 3 de la migration) : le compte d'installation est recréé avec
|
||||
son jeton public, à régénérer aussitôt.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
| --- | --- |
|
||||
| `database/migration_v7_9e_dsi_comptes_journal.sql` | Rôle DSI, `compte_acces`, `journal_activite`, trois vues, compte d'installation |
|
||||
| `app/Core/Journal.php` | Écriture des traces ; connexion posée par `app/bootstrap.php` |
|
||||
| `app/Core/Access.php` | `ROLE_DSI`, `openSession()` (remplace `select()`), relecture du compte, journalisation des refus |
|
||||
| `app/Models/CompteModel.php` | Jetons : génération, empreinte, authentification, cycle de vie des comptes |
|
||||
| `app/Models/JournalModel.php` | Lecture paginée, statistiques, compteur d'échecs, purge |
|
||||
| `app/Controllers/RoleController.php` | Connexion par jeton (champ et lien), déconnexion, frein anti-force-brute |
|
||||
| `app/Controllers/DsiController.php` | Les sept endpoints de l'espace DSI |
|
||||
| `dsi.php`, `api/dsi_*.php` | Points d'entrée minces |
|
||||
| `app/Views/pages/role.php` | Écran de connexion (le sélecteur de rôle a disparu) |
|
||||
| `app/Views/partials/dsi.php`, `assets/js/dsi.js` | Interface de l'espace DSI |
|
||||
|
||||
## Ce que la V7.11 ne fait pas
|
||||
|
||||
- **Pas d'annuaire ni de SSO** : le jeton reste un secret partagé hors de l'application (courriel,
|
||||
papier). Un raccordement LDAP ou OpenID Connect resterait à faire, et ne toucherait que
|
||||
`RoleController` et `CompteModel`.
|
||||
- **Pas d'expiration automatique des jetons** : un jeton vit tant que la DSI ne le régénère pas. Une
|
||||
date de péremption serait une colonne de plus sur `compte_acces` et une condition de plus dans
|
||||
`v_acces_compte`.
|
||||
- **Pas d'export du journal** : il se consulte à l'écran, filtre par filtre. Un export CSV
|
||||
demanderait un endpoint supplémentaire.
|
||||
- **Pas de purge automatique** : la purge est déclenchée à la main par la DSI, et journalisée.
|
||||
133
docs/README_V7_12.md
Normal file
133
docs/README_V7_12.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# PTA V7.12 — Second facteur pour les comptes Service Enfance et DSI
|
||||
|
||||
## Le problème traité
|
||||
|
||||
La V7.11 a remplacé le sélecteur de rôle par un jeton personnel. C'était un progrès décisif, mais un
|
||||
jeton reste **un secret qui circule** : il est produit par la DSI, transmis par courriel ou sur
|
||||
papier, parfois recopié dans un carnet. Un jeton intercepté suffisait à valider les plannings de
|
||||
toute la collectivité (Service Enfance), voire à distribuer de nouveaux accès (DSI).
|
||||
|
||||
Le risque n'est pas le même selon le compte. Un agent ne peut que lire son propre planning ; un
|
||||
responsable ne touche qu'aux brouillons de son lieu. Ces deux rôles-là restent à un seul facteur —
|
||||
alourdir leur connexion coûterait plus qu'il ne protégerait.
|
||||
|
||||
## Ce qui change
|
||||
|
||||
### 1. Deux facteurs, mais seulement là où c'est justifié
|
||||
|
||||
| Rôle | Jeton | Mot de passe |
|
||||
| --- | --- | --- |
|
||||
| Agent | oui | — |
|
||||
| Responsable de structure | oui | — |
|
||||
| **Service Enfance** | oui | **oui** |
|
||||
| **DSI** | oui | **oui** |
|
||||
|
||||
La règle vit **en base**, dans la vue `v_acces_compte` (`mot_de_passe_requis`), comme le reste des
|
||||
droits depuis la V7.8 : étendre le second facteur à un autre rôle est une modification SQL, pas un
|
||||
redéploiement.
|
||||
|
||||
Les deux secrets sont volontairement de nature différente : le jeton est **remis** par la DSI, le
|
||||
mot de passe est **choisi** par le titulaire et n'est jamais affiché nulle part.
|
||||
|
||||
### 2. Le mot de passe se choisit à la première connexion
|
||||
|
||||
La DSI ne pose aucun mot de passe initial, et ne peut donc en connaître aucun. Le parcours :
|
||||
|
||||
1. la DSI crée le compte et remet le jeton ;
|
||||
2. à la première connexion, le jeton est reconnu et l'écran **impose de choisir un mot de passe** ;
|
||||
3. aux connexions suivantes : jeton, puis mot de passe.
|
||||
|
||||
Entre les deux étapes, **rien n'est ouvert** : la session ne porte qu'une attente horodatée, périmée
|
||||
au bout de cinq minutes. Un jeton validé sans mot de passe ne donne accès à aucune page — vérifié :
|
||||
`dsi.php` renvoie alors vers l'écran de connexion.
|
||||
|
||||
Le compte est **relu en base entre les deux étapes** : si la DSI révoque le compte pendant que
|
||||
l'utilisateur tape son mot de passe, la session ne s'ouvre pas.
|
||||
|
||||
### 3. Politique : la longueur, et rien d'autre
|
||||
|
||||
- **12 caractères minimum**, 72 maximum. Aucune règle de composition : exiger une majuscule et un
|
||||
chiffre produit `Motdepasse1`, pas un secret. Une phrase dont on se souvient vaut mieux.
|
||||
- La borne haute n'est pas arbitraire : `password_hash()` utilise bcrypt, qui **ignore
|
||||
silencieusement** tout ce qui dépasse 72 octets. Mieux vaut refuser que laisser croire.
|
||||
- Le jeton lui-même est refusé comme mot de passe : le recopier ramènerait les deux facteurs à un
|
||||
seul secret.
|
||||
- Hachage par `password_hash()` (bcrypt), avec remise à niveau automatique si PHP change
|
||||
d'algorithme par défaut. C'est un secret choisi par un humain, donc attaquable par dictionnaire :
|
||||
contrairement au jeton, le hachage lent a ici tout son sens.
|
||||
|
||||
### 4. Changement et oubli
|
||||
|
||||
- **Changer son mot de passe** : bouton « Mot de passe » dans l'en-tête, visible des seuls comptes
|
||||
concernés. L'ancien mot de passe est exigé — sans quoi un poste laissé ouvert suffirait à
|
||||
confisquer un compte.
|
||||
- **Oubli** : la DSI **efface** le mot de passe depuis sa page (bouton « Effacer le mot de passe »).
|
||||
Elle ne le remplace pas : le titulaire en choisit un nouveau à sa connexion suivante, avec son
|
||||
jeton inchangé. La DSI ne connaît donc jamais le mot de passe de personne, pas même à l'instant de
|
||||
la réinitialisation.
|
||||
- La colonne « État » de la liste des comptes affiche « Mot de passe défini » ou « Mot de passe à
|
||||
définir ». Le second n'est pas une anomalie : c'est l'état normal d'un compte neuf.
|
||||
|
||||
### 5. Le frein anti-force-brute couvre les deux facteurs
|
||||
|
||||
Le compteur du journal (`JournalModel::echecsRecents()`) additionne désormais `JETON_REFUSE` et
|
||||
`MOT_DE_PASSE_REFUSE` : essayer mille mots de passe derrière un jeton volé bute sur le même mur
|
||||
qu'essayer mille jetons — 10 échecs en 15 minutes par adresse IP, puis plus rien n'est comparé.
|
||||
|
||||
### 6. Ce que le journal enregistre en plus
|
||||
|
||||
| Action | Catégorie | Quand |
|
||||
| --- | --- | --- |
|
||||
| `JETON_ACCEPTE_ATTENTE_MDP` | `CONNEXION` | Jeton reconnu, mot de passe attendu |
|
||||
| `MOT_DE_PASSE_REFUSE` | `CONNEXION` | Mot de passe incorrect derrière un jeton valide |
|
||||
| `MOT_DE_PASSE_DEFINI` | `ECRITURE` | Premier choix par le titulaire |
|
||||
| `MOT_DE_PASSE_MODIFIE` | `ECRITURE` | Changement par le titulaire |
|
||||
| `MOT_DE_PASSE_REINITIALISE` | `ADMINISTRATION` | Effacement par la DSI |
|
||||
|
||||
`CONNEXION_REUSSIE` porte en contexte les facteurs réellement présentés (`["jeton"]` ou
|
||||
`["jeton","mot_de_passe"]`) : le journal distingue donc une connexion à un facteur d'une connexion à
|
||||
deux.
|
||||
|
||||
## Mise en service
|
||||
|
||||
Rien à faire hors la migration `database/migration_v7_9f_mot_de_passe_admin.sql`, appliquée
|
||||
automatiquement au démarrage. **Les comptes Service Enfance et DSI existants n'ont pas de mot de
|
||||
passe** : leur prochaine connexion, jeton en main, leur en fera choisir un. Aucun jeton n'est
|
||||
invalidé, aucun compte n'est bloqué.
|
||||
|
||||
Prévenez les titulaires concernés : ils verront un écran qu'ils n'attendent pas.
|
||||
|
||||
### Si le dernier accès DSI perd son mot de passe
|
||||
|
||||
Le cas ne se résout pas dans l'interface, puisqu'il faudrait être connecté pour le faire. En SQL :
|
||||
|
||||
```sql
|
||||
UPDATE compte_acces SET mot_de_passe_hash = NULL, date_mot_de_passe = NULL WHERE id_compte = 1;
|
||||
```
|
||||
|
||||
La connexion suivante, avec le jeton, redemandera d'en définir un. Si le jeton lui-même est perdu,
|
||||
voir la procédure de `README_V7_11.md`.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
| --- | --- |
|
||||
| `database/migration_v7_9f_mot_de_passe_admin.sql` | Colonnes `mot_de_passe_hash` / `date_mot_de_passe`, vue `v_acces_compte` enrichie de `mot_de_passe_requis` et `mot_de_passe_defini` |
|
||||
| `app/Controllers/RoleController.php` | Connexion en deux étapes, définition, vérification, changement |
|
||||
| `app/Models/CompteModel.php` | Politique, hachage, vérification, réinitialisation |
|
||||
| `app/Models/JournalModel.php` | Frein étendu au second facteur |
|
||||
| `app/Controllers/DsiController.php`, `api/dsi_compte_mot_de_passe.php` | Effacement par la DSI |
|
||||
| `app/Views/pages/role.php` | Les quatre états de l'écran de connexion |
|
||||
| `app/Views/layouts/header.php` | Lien « Mot de passe », pour les seuls comptes concernés |
|
||||
|
||||
## Ce que la V7.12 ne fait pas
|
||||
|
||||
- **Pas de verrouillage du compte** après N échecs : seul le frein par adresse IP s'applique. Un
|
||||
verrouillage par compte permettrait à quiconque connaît un jeton de bloquer volontairement un
|
||||
administrateur.
|
||||
- **Pas d'expiration ni d'historique des mots de passe** : forcer un changement tous les trois mois
|
||||
produit `Motdepasse-Mars`, `Motdepasse-Juin`. Le mot de passe se change quand il y a une raison.
|
||||
- **Pas de réinitialisation par courriel** : il n'y a pas d'envoi de courriel dans PTA, et la boîte
|
||||
aux lettres deviendrait le vrai facteur d'authentification.
|
||||
- **Pas de TOTP** : le jeton joue déjà le rôle du « quelque chose que l'on détient ». Un
|
||||
authentificateur mobile serait le prolongement naturel si le besoin s'en faisait sentir.
|
||||
51
docs/README_V7_13.md
Normal file
51
docs/README_V7_13.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# PTA V7.13 — Nom et prénom modifiables sur la fiche agent
|
||||
|
||||
## Le problème traité
|
||||
|
||||
La modale « Modifier les informations de l’agent » couvrait tout sauf l’identité : coordonnées,
|
||||
diplôme, poste, contrat, lieu principal, commentaire PTA. Le nom et le prénom, eux, n’étaient saisis
|
||||
qu’à la création.
|
||||
|
||||
Or ils changent : mariage, divorce, nom d’usage, ou plus prosaïquement une faute de frappe lors de la
|
||||
saisie initiale. La seule correction possible passait par une requête SQL — donc par quelqu’un qui a
|
||||
l’accès à la base, ce que la V7.11 s’était justement employée à restreindre.
|
||||
|
||||
## Ce qui change
|
||||
|
||||
Deux champs, **Prénom** et **Nom**, ouvrent la modale d’édition. Ils sont obligatoires et bornés à
|
||||
100 caractères, comme à la création.
|
||||
|
||||
Restent volontairement hors d’atteinte :
|
||||
|
||||
- le **matricule** — c’est la clé de rapprochement avec les outils RH, et les plannings comme les PDF
|
||||
déjà produits s’y réfèrent ;
|
||||
- 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.
|
||||
|
||||
Un renommage se lit **directement dans le journal**, sans avoir à comparer deux lignes :
|
||||
|
||||
```
|
||||
AGENT_MODIFIE Modification de la fiche de l’agent Marc-Antoine LOUIS, renommé depuis « Marc Louis ».
|
||||
{"id_structure":1,"type_contrat":"PERMANENT","identite_precedente":"Marc Louis"}
|
||||
```
|
||||
|
||||
L’identité précédente n’apparaît que s’il y a effectivement eu changement : une modification de
|
||||
coordonnées garde la trace courte.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
| --- | --- |
|
||||
| `app/Views/partials/administration.php` | Les deux champs dans la modale, `data-nom` / `data-prenom` sur la ligne du tableau |
|
||||
| `assets/js/administration.js` | Pré-remplissage depuis les `data-*`, envoi, rafraîchissement de la ligne |
|
||||
| `app/Controllers/AgentController.php` | Validation, fiche relue avant modification pour repérer le renommage |
|
||||
| `app/Models/AgentModel.php` | `nom` et `prenom` dans l’`UPDATE` de `updateProfile()` |
|
||||
|
||||
Aucune migration : les colonnes existent depuis le socle.
|
||||
|
||||
## Point d’attention
|
||||
|
||||
Les `data-*` de la ligne du tableau sont la SOURCE de la modale : elle s’ouvre à partir d’eux, sans
|
||||
appel réseau. Ajouter un champ modifiable impose donc de le poser à trois endroits — le partiel
|
||||
(attribut `data-*`), `setAgentRowData()` et `openAgentProfileModal()` — sans quoi il s’ouvre vide
|
||||
puis écrase la valeur existante à l’enregistrement.
|
||||
80
docs/README_V7_14.md
Normal file
80
docs/README_V7_14.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# PTA V7.14 — Groupe « Management » : les agents et les lieux ont chacun leur onglet
|
||||
|
||||
## Le problème traité
|
||||
|
||||
L’onglet « Administration » réunissait trois objets sans rapport les uns avec les autres :
|
||||
|
||||
- le référentiel des **agents** (création, fiches, rattachements) ;
|
||||
- le référentiel des **lieux d’affectation** (création, type périscolaire/extrascolaire) ;
|
||||
- les **horaires de couverture attendus**, qui ne décrivent ni une personne ni un lieu mais une
|
||||
exigence de présence.
|
||||
|
||||
L’écran était aussi organisé à l’envers : les deux formulaires de création côte à côte en haut, puis
|
||||
les deux tableaux plus bas, séparés par les horaires de couverture. Créer un agent puis vérifier sa
|
||||
fiche demandait de traverser tout l’écran.
|
||||
|
||||
## Ce qui change
|
||||
|
||||
### 1. Un groupe de navigation « Management », deux onglets
|
||||
|
||||
La barre de navigation gagne un cinquième groupe, au même titre que « Piloter », « Planifier »,
|
||||
« Consulter » et « Paramétrer » :
|
||||
|
||||
| Groupe | Onglets |
|
||||
| --- | --- |
|
||||
| Piloter | Tableau de bord, À valider, Couverture |
|
||||
| Planifier | Saisie, Équipes |
|
||||
| Consulter | Par agent, Par lieu, Vue annuelle, PTA détaillé |
|
||||
| **Management** | **Agents**, **Lieux d’affectation** |
|
||||
| Paramétrer | Vacances, Administration, Accès & journaux |
|
||||
|
||||
Chaque onglet porte **la création puis la liste** de son objet : « Ajouter un agent » puis
|
||||
« Informations des agents » ; « Ajouter un lieu » puis « Paramètres des lieux ». C’est l’ordre du
|
||||
travail réel.
|
||||
|
||||
Le groupe se place volontairement **avant** « Paramétrer » : on y crée et corrige au fil de l’eau,
|
||||
là où « Paramétrer » se règle une fois pour l’année.
|
||||
|
||||
### 2. « Administration » ne garde que la couverture
|
||||
|
||||
Il ne reste que l’encadré « Horaires de couverture des lieux » — le paramètre que contrôle l’onglet
|
||||
« Couverture ». Le sous-titre de la page le dit, et la page ne charge plus que le module `coverage` :
|
||||
les 578 lignes du module `administration` partent avec les deux nouveaux onglets.
|
||||
|
||||
### 3. Rien n’a changé côté serveur
|
||||
|
||||
Endpoints, gardes, contrôles et modules JS sont **identiques** : c’est un déplacement de markup, pas
|
||||
une réécriture. Les identifiants HTML — points d’ancrage du module `administration` — sont conservés
|
||||
à la lettre, et ce module fonctionne indifféremment sur l’un ou l’autre onglet : ses écouteurs sont
|
||||
tous posés en `?.`, donc sans effet quand l’élément visé n’est pas sur la page.
|
||||
|
||||
Deux adaptations d’affichage : le raccourci du tableau de bord pointe sur « Agents », et le guide
|
||||
d’utilisation renvoie aux nouveaux onglets. La tuile « Lieux à paramétrer » continue de mener à
|
||||
« Administration » : elle parle bien de couverture.
|
||||
|
||||
## Une singularité de nommage à connaître
|
||||
|
||||
Les deux méthodes de `PageController` s’appellent `manage_agents()` et `manage_structures()`, en
|
||||
snake_case, à rebours de tout le reste du projet. Ce n’est pas un oubli : le nom de la méthode **est**
|
||||
la clé de page passée à `runPage()` et inscrite dans `acces_role_page`, où les clés sont en
|
||||
minuscules (`dashboard`, `coverage`, `pending`…). Garder les deux identiques prime sur l’uniformité
|
||||
de style — c’est cette identité qui permet à PTA de se passer de routeur.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
| --- | --- |
|
||||
| `database/migration_v7_9g_management.sql` | Droits d’accès aux deux pages pour le Service Enfance |
|
||||
| `manage_agents.php`, `manage_structures.php` | Points d’entrée |
|
||||
| `app/Views/pages/manage_*.php`, `app/Views/partials/manage_*.php` | Vues et markup déplacé |
|
||||
| `app/Views/partials/administration.php` | Ne conserve que les horaires de couverture |
|
||||
| `app/Controllers/PageController.php` | `manage_agents()`, `manage_structures()`, `administration()` allégé |
|
||||
| `app/Views/layouts/header.php` | Groupe « Management » dans `$navigationGroups` |
|
||||
| `app/Views/partials/dashboard.php`, `app/Views/partials/faq.php` | Renvois mis à jour |
|
||||
|
||||
## Point d’attention
|
||||
|
||||
Sans la migration, les pages sont refusées et les onglets n’apparaissent pas — c’est la règle de PTA
|
||||
depuis la V7.8 : **les pages autorisées sont lues en base**, jamais écrites en PHP. La migration
|
||||
supprime au passage la ligne `management` d’une version intermédiaire : une page inconnue laissée
|
||||
dans `acces_role_page` finirait dans le groupe de repêchage « Autres » de la barre de navigation.
|
||||
90
docs/README_V7_15.md
Normal file
90
docs/README_V7_15.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# PTA V7.15 — Les affectations sont bornées par le contrat de l’agent
|
||||
|
||||
## Le problème traité
|
||||
|
||||
La fiche agent portait `date_debut_contrat` et `date_fin_contrat` depuis l’origine — saisies,
|
||||
contrôlées l’une par rapport à l’autre, affichées dans les écrans et les PDF. Mais **aucun contrôle
|
||||
de planification ne les lisait** : rien n’empêchait de planifier un agent trois mois avant son
|
||||
arrivée, ou un contrat temporaire l’année suivant son départ.
|
||||
|
||||
Les conséquences n’étaient pas cosmétiques : ces heures entraient dans les cumuls annuels, dans le
|
||||
quota, dans les contrôles de couverture — un lieu pouvait passer pour couvert par un agent qui
|
||||
n’était pas encore embauché.
|
||||
|
||||
## Ce qui change
|
||||
|
||||
### 1. Toute écriture de créneau est bornée
|
||||
|
||||
La règle est posée sur **tous** les chemins qui créent ou déplacent une affectation :
|
||||
|
||||
| Chemin | Ce qui est contrôlé |
|
||||
| --- | --- |
|
||||
| Enregistrement d’un brouillon | Chaque date saisie |
|
||||
| Modification d’un créneau (fiche agent) | La nouvelle date |
|
||||
| Déplacement / duplication d’un créneau | La date cible |
|
||||
| Copie d’une semaine entière | Les cinq jours ouvrés de la semaine cible |
|
||||
| Application d’une semaine type | Les cinq jours ouvrés de la semaine visée |
|
||||
| Duplication sur une période | Les deux bornes de la période |
|
||||
|
||||
Le refus est un **422** portant `code: HORS_CONTRAT`, la liste des dates refusées, et une phrase qui
|
||||
nomme la période réellement couverte :
|
||||
|
||||
> Marc Louis n’est pas sous contrat à cette période : son contrat court à partir du 04/05/2026.
|
||||
> Date(s) refusée(s) : 02/03/2026.
|
||||
|
||||
La règle vaut pour **tous les motifs**, congés et absences compris : hors contrat, il n’y a rien à
|
||||
décompter.
|
||||
|
||||
Une borne absente **ouvre**, elle ne ferme jamais : pas de date de fin signifie contrat à durée
|
||||
indéterminée ; pas de date de début (fiche ancienne) signifie qu’aucune borne basse n’est opposée,
|
||||
plutôt que d’inventer une date.
|
||||
|
||||
### 2. La validation révèle les affectations déjà hors contrat
|
||||
|
||||
Les gardes ci-dessus empêchent d’en créer de nouvelles ; elles ne disent rien de celles qui
|
||||
existaient avant. Un contrôle bloquant `HORS_CONTRAT` rejoint donc les quatre contrôles de
|
||||
validation existants, à la validation d’une semaine comme à celle d’une année entière :
|
||||
|
||||
> L’affectation #824 du 2026-05-11 est en dehors des dates de contrat de l’agent.
|
||||
|
||||
La comparaison est faite en SQL sur la fiche agent jointe : c’est l’état du contrat **au moment du
|
||||
contrôle** qui fait foi.
|
||||
|
||||
### 3. Le contrat ne peut plus glisser sous des affectations existantes
|
||||
|
||||
Contrôle miroir, sans lequel le premier ne servirait à rien : modifier les dates de contrat d’un
|
||||
agent est refusé si cela laisserait des affectations au-dehors. Le message dit combien et sur quelle
|
||||
période, pour qu’on sache quoi retirer :
|
||||
|
||||
> Ces dates de contrat laisseraient 6 affectation(s) en dehors du contrat, entre le 11/05/2026 et le
|
||||
> 14/08/2026. Supprimez-les d’abord, ou élargissez les dates.
|
||||
|
||||
## Vérifications effectuées
|
||||
|
||||
Onze cas exécutés sur la pile, tous conformes : saisie avant le début de contrat (refusée), dans le
|
||||
contrat (acceptée), sur la semaine à cheval contenant le premier jour (acceptée), copie de semaine
|
||||
hors contrat, saisie après la date de fin, déplacement et modification de créneau vers une date hors
|
||||
contrat, raccourcissement de contrat sous des affectations existantes, contrôle de validation sur une
|
||||
donnée devenue illégitime, application d’une semaine type sur une semaine puis sur une période hors
|
||||
contrat.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
| --- | --- |
|
||||
| `app/Models/AgentModel.php` | `contractRange()`, `contractCoversDate()`, `datesOutsideContract()`, `contractViolationMessage()`, `entriesOutsideContract()` |
|
||||
| `app/Controllers/PlanningController.php` | Garde sur les quatre chemins d’écriture, contrôle de validation |
|
||||
| `app/Controllers/WeekTemplateController.php` | Garde sur l’application à une semaine et sur une période |
|
||||
| `app/Controllers/AnnualOverviewController.php` | Même contrôle à la validation annuelle |
|
||||
| `app/Controllers/AgentController.php` | Refus d’un contrat qui orphelinerait des affectations |
|
||||
| `app/Models/PlanningModel.php` | `entriesOutsideContract()` pour le contrôle de validation |
|
||||
|
||||
Aucune migration : les colonnes existent depuis le socle, seule leur exploitation change.
|
||||
|
||||
## Point d’attention
|
||||
|
||||
La garde est **dupliquée** dans `PlanningController` et `WeekTemplateController`, faute de base
|
||||
commune entre contrôleurs — comme l’est déjà `parseWeek()`. Ce qui n’est pas dupliqué, c’est le
|
||||
message : il est construit par `AgentModel::contractViolationMessage()`, pour que l’utilisateur lise
|
||||
la même phrase quel que soit l’écran. **Toute nouvelle voie d’écriture de créneaux doit poser la même
|
||||
garde** — le contrôle de validation n’est qu’un filet, pas un substitut.
|
||||
Reference in New Issue
Block a user