La piste d'audit était écrite à chaque mutation depuis WP-01, et lisible nulle
part. Une piste que personne ne peut consulter ne prouve rien le jour où on le
lui demande.
L'écran ne porte aucune action : le journal est append-only, imposé par un
trigger, et cette page ne fait que le lire. La capacité `audit.view` est
distincte de l'administration — on confie la lecture de la piste à un contrôleur
sans lui donner le droit de modifier ce qu'elle enregistre.
Seuls les noms des champs modifiés s'affichent, pas leurs valeurs. Le journal
enregistre bien l'avant et l'après en clair, c'est ce qui le rend opposable ;
mais les étaler dans une liste exposerait un salaire ou un motif médical à
quiconque ouvre l'écran. La liste dit ce qui a changé, la fiche dit quoi, sous
ses propres autorisations.
Les listes de filtres se déduisent du journal lui-même plutôt que d'une
énumération tenue à la main : une action ajoutée par un lot futur y apparaît
seule, et une énumération figée finirait par masquer ce qu'elle omet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux des cinq vues de §9 manquaient.
La vue des présences répond à une question que les quatre autres posent mal :
qui est là aujourd'hui. La grille montre des créneaux, pas des personnes ; il
fallait la lire colonne par colonne et compter de tête.
Le planning à afficher est une mise en page pour la feuille punaisée en salle de
pause — toutes les équipes à la suite, sans panneau d'édition ni compteurs, un
aplat par créneau avec l'horaire et le code du poste écrits. Sans la capacité de
voir le non publié, la feuille ne la contourne pas : imprimer un brouillon
revient à le publier sur un mur.
La règle « absent ou présent » part dans le domaine plutôt que dans les écrans.
Les deux vues en tirent la même conclusion, et un export futur en tirera la
même : deux implémentations finiraient par se contredire sur le seul cas qui
compte, celui du congé validé après la construction de la semaine. Les créneaux
restent alors au planning, et l'absence l'emporte — afficher « présent »
enverrait un responsable chercher en rayon quelqu'un qui est chez lui.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le calendrier portait tout : les demandes en attente, les décisions rendues et
celles qui avaient expiré, distinguées par un simple badge. Une file d'attente
se lit mal dans un calendrier — elle se lit par ancienneté, et le calendrier
range par date d'absence.
Quatre routes désormais : le calendrier, la file à traiter, les traitées et les
expirées. Le calendrier a déménagé sous /absences plutôt que d'être dupliqué, et
/conges redirige en conservant le mois : un lien partagé dans un message
continue d'ouvrir la bonne page.
Les trois files partagent une requête et un rendu. C'est délibéré : la règle qui
masque le motif d'une absence Sécurité sociale vaut pour les trois, et trois
copies de cette règle garantiraient qu'une finisse par diverger. Une donnée de
santé qui ne fuit que sur l'écran « traitées » reste une fuite.
La file à traiter classe par ancienneté de demande, les files closes par date
d'absence décroissante : on cherche d'un côté ce qui attend depuis trop
longtemps, de l'autre ce qui vient d'être décidé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Emplois, étiquettes et types d'absence vivaient en base sans écran : le seed
les posait, et rien ne permettait ensuite d'en ajouter un. Ils s'éditent
désormais depuis les réglages.
Trois choix portent la conformité plutôt que le confort :
- Archiver, jamais supprimer. Un emploi figure au registre du personnel, une
étiquette colore des semaines closes, un type d'absence justifie des
écritures de compteur. Supprimer l'un des trois rendrait illisible un dossier
dont la conservation court encore.
- Le code Silae manquant est signalé. Un type sans code fait échouer l'export
de la période ; l'écran le dit avant l'échéance de paie plutôt qu'après.
- La convention se réédite, elle ne se modifie pas. Chaque enregistrement crée
une version datée : une paie de mars doit rester reproductible après un
changement de seuil en juin. L'origine de chaque valeur — ordre public,
convention, accord d'entreprise — est affichée, parce que les trois ne se
corrigent pas de la même main.
Les mutations de types d'absence passent par settings.agreement.manage : le
catalogue §5 ne déclare pas de code dédié, et ces drapeaux relèvent du
paramétrage légal. Inventer une capacité aurait créé un droit sans rien
protéger de neuf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Créer un salarié demandait cinq champs. Le contrat, la déclaration et le
registre en réclament une vingtaine, et rien ne permettait de les saisir :
le sexe, le nom de naissance, le pays et le département de naissance, la
situation de famille, les personnes à charge, le téléphone fixe, le
complément d'adresse et l'heure d'embauche n'existaient pas en base.
La migration les ajoute, toutes facultatives : un dossier incomplet doit
pouvoir exister — c'est au registre de signaler ce qui lui manque, pas à
la base de refuser l'embauche. Seul l'envoi des plannings par SMS fait
exception : c'est un consentement, donc faux par défaut, la charge de la
preuve pesant sur l'employeur.
Le pays et le département de naissance sont des colonnes à part et non une
commune saisie librement : la déclaration sociale les demande séparément,
et les rétro-extraire échouerait au premier « Bar-le-Duc (Meuse) ».
Le formulaire devient un panneau latéral : une trentaine de champs posés
au centre masquaient l'annuaire, et on embauche en regardant qui est déjà
là. Le matricule y est proposé à la suite du dernier, attribué dans la
transaction pour que deux embauches simultanées ne tombent pas sur le même
rang. Le nom de naissance vaut le nom de famille quand il n'en diffère
pas, plutôt que d'imposer une ressaisie à l'immense majorité des dossiers.
Le responsable hiérarchique et la politique RTT se posent enfin à
l'embauche : les deux modèles existaient sans qu'aucun écran ne les
alimente.
Le registre unique du personnel gagne sa colonne « sexe » et se tient
désormais au nom de naissance — le nom d'usage peut changer sans que la
personne change.
La migration s'applique au redéploiement : l'entrypoint du conteneur passe
`prisma migrate deploy` avant de démarrer le serveur. Elle n'ajoute que des
colonnes nullables et deux types énumérés, sans réécriture de table.
Écrite et validée contre le schéma, non exécutée ici : aucune base n'était
ouverte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Créer un salarié demandait cinq champs. Le contrat, la déclaration et le
registre en réclament une vingtaine, et rien ne permettait de les saisir :
le sexe, le nom de naissance, le pays et le département de naissance, la
situation de famille, les personnes à charge, le téléphone fixe, le
complément d'adresse et l'heure d'embauche n'existaient pas en base.
La migration les ajoute, toutes facultatives : un dossier incomplet doit
pouvoir exister — c'est au registre de signaler ce qui lui manque, pas à
la base de refuser l'embauche. Seul l'envoi des plannings par SMS fait
exception : c'est un consentement, donc faux par défaut, la charge de la
preuve pesant sur l'employeur.
Le pays et le département de naissance sont des colonnes à part et non une
commune saisie librement : la déclaration sociale les demande séparément,
et les rétro-extraire échouerait au premier « Bar-le-Duc (Meuse) ».
Le formulaire devient un panneau latéral : une trentaine de champs posés
au centre masquaient l'annuaire, et on embauche en regardant qui est déjà
là. Le matricule y est proposé à la suite du dernier, attribué dans la
transaction pour que deux embauches simultanées ne tombent pas sur le même
rang. Le nom de naissance vaut le nom de famille quand il n'en diffère
pas, plutôt que d'imposer une ressaisie à l'immense majorité des dossiers.
Le responsable hiérarchique et la politique RTT se posent enfin à
l'embauche : les deux modèles existaient sans qu'aucun écran ne les
alimente.
Le registre unique du personnel gagne sa colonne « sexe » et se tient
désormais au nom de naissance — le nom d'usage peut changer sans que la
personne change.
Migration écrite et validée contre le schéma, non appliquée : aucune base
n'était ouverte ici. À passer avec `pnpm db:migrate`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le bouton menait au registre de paramétrage juridique — un autre document,
qui ne répond à aucune demande d'inspection. Le registre unique du
personnel n'existait pas.
Il se tient par établissement : celui-ci est demandé avant l'édition
plutôt que déduit, car en tirer un au hasard pour une entreprise qui
compte trente sites ne répondrait à rien.
Une ligne par contrat et non par personne : un salarié réembauché a deux
entrées et deux sorties, et les fondre effacerait l'interruption. Les
partis y figurent aussi — c'est l'historique que l'inspection vient
chercher.
Les mentions manquantes sont annoncées avant le téléchargement, et
comptées par salarié parce que la contravention l'est aussi. Découvrir un
registre incomplet en l'ouvrant, c'est le découvrir devant l'inspection.
L'édition est journalisée : le document rassemble l'identité, la
nationalité et le parcours de tout le personnel d'un site.
Une réserve, portée par le code et par le document : PlanFlow ne collecte
pas le sexe, mention pourtant exigée. La colonne reste vide et alimente le
décompte des dossiers incomplets, en attendant le champ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'écran s'ouvrait sur deux paragraphes de doctrine avant de montrer quoi
que ce soit. Le formulaire de saisie demande déjà valeur, source, date
d'effet, population et approbateur : la règle se lit dans les champs, où
elle s'applique, plutôt qu'au-dessus, où elle se saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le formulaire d'embauche, posé au bas de la liste, occupait plus de place
que l'effectif qu'on vient consulter — une quinzaine de champs déployés en
permanence pour un geste occasionnel. Il s'ouvre désormais en modale,
depuis le bouton d'en-tête.
`<dialog>` natif plutôt qu'un panneau maison : le piège de focus, la
fermeture par Échap et le fond inerte viennent avec, et une `<div>` doit
les réimplémenter sans jamais les tenir tout à fait.
Le tableau perd sa carte et ses fonds : une liste de personnes se lit
mieux sans cadre autour. Les colonnes suivent l'annuaire de référence —
collaborateur, rôle, email, mobile, rattachement, invitation. Le contrat
quitte le tableau, où il doublait le filtre qui le cherche déjà ; son
absence reste signalée là où elle compte, dans le rattachement.
Le décompte ne s'affiche plus que filtré : « 87 salariés » au-dessus de 87
lignes n'apprend rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'onglet montrait les équipes et le périmètre sans jamais permettre de les
changer : un salarié rattaché à la mauvaise équipe n'apparaissait pas sur
la bonne grille, et rien à l'écran n'y remédiait.
Deux formulaires et deux capacités, parce que ce sont deux décisions.
Élargir un périmètre revient à donner accès à des dossiers et des
plannings qu'on n'avait pas : c'est distribuer des droits, donc
`settings.roles.manage`. Rattacher à une équipe relève de l'organisation
du travail, donc `settings.teams.manage`. Les réunir donnerait à l'une le
pouvoir de l'autre.
Le périmètre est remplacé, pas fusionné : un périmètre qui ne fait que
grandir ne se réduit jamais, et retirer un établissement resterait sans
effet — l'exact contraire de ce que l'écran promet. Un périmètre vide sans
accès généralisé est refusé : il ne laisserait plus rien voir.
L'établissement de rattachement reste en lecture seule. Il est porté par
le contrat, et le changer sans avenant ferait diverger le planning du
document opposable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Créer un salarié demandait quatre champs et produisait un dossier vide :
sans contrat il n'apparaît sur aucune grille et ne se déclare pas, sans
équipe il ne s'ordonne nulle part — et jusqu'ici aucun écran ne permettait
de rattraper l'un ni l'autre.
Le contrat est proposé coché, et reste décochable : un remplaçant se
saisit parfois avant que son établissement soit tranché.
Dossier, contrat et rattachement entrent dans la même transaction. Créés
séparément, un incident au milieu laisserait exactement le dossier
incomplet que ce changement vise à supprimer.
Ouvrir un contrat reste une capacité distincte de celle de créer un
dossier : un gestionnaire d'annuaire n'engage pas l'entreprise. Le refus
le dit, et propose de créer le salarié sans contrat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six colonnes et rien pour s'y retrouver : au-delà de vingt salariés,
l'effectif se parcourait au défilement. Recherche par prénom, nom ou
matricule, et quatre filtres — établissement, rôle, type de contrat,
présence — plus le tri.
Les critères vivent dans l'adresse et non dans le composant : un effectif
filtré s'envoie par lien, et le retour arrière défait le dernier choix
plutôt que de vider la page. Ils sont appliqués en base, pas après lecture :
plusieurs centaines de dossiers ne se filtrent pas en mémoire à chaque
affichage.
Le tri par nom reste en mémoire, lui : Postgres range « Étienne » après
« Etchegaray », un lecteur français avant.
Les colonnes suivent : rôle, mobile, rattachement et état de l'invitation
remplacent le matricule, qui ne s'affiche plus que sous le nom des salariés
sans compte applicatif — ceux pour qui c'est la seule adresse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`createContractAction` et `createAmendmentAction` existaient depuis le
début sans qu'aucun écran ne les appelle : du code mort côté interface, et
un contrat qu'on ne pouvait ni ouvrir ni modifier une fois le salarié créé.
Trois gestes sur le contrat en cours — lire le détail, déclarer un
changement, terminer — et l'ouverture d'un contrat quand il n'y en a pas.
`endContractAction` date et clôt le contrat plutôt que de le supprimer :
un contrôle porte sur une période révolue, et l'effacer effacerait la
preuve que le salarié a travaillé, ainsi que la base de son solde de tout
compte.
Le formulaire de forfait jours ne découvre la convention individuelle et
sa date d'accord qu'une fois le forfait choisi : la règle métier les exige,
les autres contrats n'en ont que faire.
Les établissements du formulaire de création se lisent avec
`members.contract.create` et non `settings.access` : ouvrir un contrat
n'est pas administrer un établissement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fiche empilait tout sur une page, en lecture seule. Le dossier personnel
existait pourtant en base — date et lieu de naissance, nationalité, adresse,
contact d'urgence, NIR et IBAN chiffrés — sans qu'aucun écran ne l'écrive :
un salarié créé restait un dossier vide que rien ne permettait de remplir.
Cinq onglets, cinq routes : une fiche s'envoie par lien et se rouvre au même
endroit. Le bandeau porte ce qui ne change pas d'un onglet à l'autre, et le
gabarit le tient une fois pour toutes.
Deux points d'écriture et non un seul. L'état civil et les coordonnées
demandent `members.edit` ; le NIR, l'IBAN et le BIC exigent en plus de
pouvoir les lire. Fondus dans un même formulaire, un profil habilité à
modifier mais pas à lire aurait renvoyé des champs vides — et effacé ce qui
ne lui avait jamais été montré. Le journal retient qu'ils ont été renseignés,
jamais leur valeur.
Les parcours qui déposaient une pièce ou ouvraient un accès passent
désormais par l'onglet qui les porte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Repris tel quel, à la demande. Le fichier décrit un autre harnais que
Claude Code : les sections outils, artifacts et mémoire n'y ont pas
d'effet, et les conventions du projet n'y figurent pas encore.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le layout applicatif calcule l'écran d'enrôlement à son propre rendu, et
un layout n'est pas rejoué par la navigation client : une fois le second
facteur activé, l'écran restait posé sur toutes les routes, quel que soit
le menu choisi.
L'action d'enrôlement ne peut pas réactualiser la route elle-même sans
emporter les codes de secours, qui ne sont affichés qu'une fois. Le
rafraîchissement est donc déclenché par l'écran des codes, sur un bouton :
c'est leur destinataire qui décide du moment où il les a notés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le seed installe un premier compte de démonstration : il ne peut pas le
créer dans un contexte locataire qui n existe pas encore. Or il se
connectait avec le rôle applicatif, soumis à la row-level security en
FORCE — l upsert Prisma (INSERT ... ON CONFLICT) se heurtait à la politique
de lecture « id = planflow_current_account() », renvoyant NULL.
Utiliser ADMIN_DATABASE_URL (compte d amorçage), comme le fait déjà le
harnais de tests d intégration (tests/integration/admin-db.ts).
Le rôle planflow_app n etait créé qu à la première initialisation du volume :
une base existante au rôle absent (ou au mot de passe changé) bloquait l app
en échec P1000 sans remède automatique.
Ajouter un service db-init, idempotent, qui rejoue docker/init-app-role.sh à
chaque up — branche ELSE re-synchronisant le mot de passe — avant que l app
ne démarre.
Plusieurs instances PostgreSQL coexistent sur le serveur : publier la base
sur 55432 par défaut (configurable via POSTGRES_PORT), sans changer le port
interne 5432 utilisé par l'application.
The root dropdowns/ copy held 21 screenshots that were missing from
Audit Combo/dropdowns/, so the two trees were not the exact duplicate
PLAN.md assumed. Move those screenshots under Audit Combo/dropdowns/
and drop the root copy, leaving one source of truth as the plan
prescribes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Screenshots, contact sheets, inventory and page-by-page audit notes
for the Combo HR platform, used as the reference for the PlanFlow
reimplementation. Personal data in the captures is blurred.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>