49c311423d7700029cfcc55757319d61cd9c8240
59
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
49c311423d |
Rattraper le référentiel des absences des instances déjà installées
installReferentials() pose les types d'absence depuis WP-02, mais ne tourne qu'à la création du compte. Une instance installée avant garde zéro type : le menu « Type » du formulaire de demande reste vide et le module de congés est inutilisable, sans qu'aucune erreur ne le signale. Déployer le correctif ne suffisait donc pas. La migration ne touche que les comptes dont le référentiel est entièrement vide — un type délibérément archivé ne doit pas réapparaître. Elle lève FORCE ROW LEVEL SECURITY le temps de la transaction, faute de compte courant. Le formulaire dit désormais que le référentiel manque et renvoie aux réglages, plutôt que d'afficher un menu vide qui laisse saisir une demande impossible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dd01fb58f4 |
Supprimer l'ascenseur des onglets, et rendre les réglages à un rail
Les onglets de section défilaient horizontalement dès que la liste dépassait la largeur. Une barre de défilement cache la moitié des entrées derrière un geste que rien n'annonce : on ne sait pas ce qu'on ne voit pas. Ils passent à la ligne — deux rangées coûtent seize pixels et montrent tout. Les réglages sortent des onglets pour un rail vertical, et eux seuls. Ce n'est pas un retour de la barre latérale : partout ailleurs la section tient en quelques entrées et le contenu garde toute la largeur. Les réglages en comptent une vingtaine ; en onglets, ils occuperaient trois rangées au-dessus de chaque écran et changeraient de forme d'une page à l'autre selon la longueur des libellés. Le rail les groupe dans l'ordre d'un paramétrage — société, organisation, travail, paie, accès, conformité — et non par ordre alphabétique : quelqu'un qui installe l'instance descend la liste. Une entrée ajoutée sans groupe reste visible en fin de rail plutôt que de disparaître. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9356c6e3f4 |
Refondre le thème autour du registre et du chiffre
L'ancien thème était crème et vert forêt, corps à 14 px, libellés secondaires à 11 px. Deux défauts s'y cumulaient : ça se lisait mal huit heures par jour, et le fond chaud décalait la perception des aplats — un jaune de poste virait au vert, un rouge d'alerte s'éteignait. Le nouveau part de ce que l'écran est vraiment : un registre qu'on lit en chiffres. Papier légèrement bleuté, encre bleu-ardoise plus dense, et un seul bleu d'acte réservé à trois emplois — le filet actif, les liens, le bouton qui engage. Tout le reste est en gris et en état, ce qui rend les alertes visibles parce qu'elles sont les seules choses colorées de la page. Lisibilité d'abord. Le corps monte à 15 px, rien ne descend sous 12, et `ink-3` passe de 0,545 à 0,50 de clarté : à l'ancienne valeur, les libellés secondaires tombaient sous 4,5:1 sur ce papier. Typographie : IBM Plex Sans, et Plex Mono pour les codes. Le choix vient du contenu — cet écran affiche des identifiants, AB-300, HS-HS25, un matricule, un département 2A. Plex a été dessiné pour la documentation technique : le 1 porte un empattement, le l est courbé, le 0 se distingue du O, et ces trois détails décident si un gestionnaire recopie le bon code. Le zéro barré est activé partout. Les fichiers sont téléchargés à la compilation et servis par l'application : `font-src 'self'` tient, et aucune requête ne part vers un tiers. La structure est portée par un **filet de marge** de 2 px plutôt que par des bordures et des aplats gris — la marge du cahier, appliquée à une interface. Il marque l'onglet ouvert, l'en-tête de carte, la section active. Il ne décore jamais : un filet dit une appartenance. Le dimanche a le sien, doublé, et une colonne teintée. C'est la seule colonne du planning qui porte une conséquence de droit — L3132-27 impose une rémunération au moins double **et** un repos compensateur équivalent. La grille le montre avant que la paie ne le découvre. Le marquage ne remplace aucune alerte : le moteur de règles reste seul juge. La barre d'application devient collante, et les boutons passent de 26 à 32 px de haut : sur une grille de quatre-vingts lignes, remonter changer de semaine est le geste le plus répété, et 26 px était sous le seuil confortable au pointeur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d90afe53a0 |
Recueillir à l'embauche ce qu'exige le contrat
Trois corrections sur le formulaire d'embauche, dont une qui explique le blocage rencontré sur le planning. L'équipe devient **requise** dès qu'un contrat est ouvert. Elle était facultative, avec un « Aucune pour l'instant » qui paraissait anodin : le planning s'ordonnant par équipe, le salarié n'apparaissait alors sur aucune grille, et rien ne le signalait avant la première semaine à couvrir. Quand l'établissement choisi n'a aucune équipe, le champ le dit et renvoie vers les réglages plutôt que de laisser deviner. Le contrôle est repris côté serveur : la validation du navigateur ne protège pas une action. Le type de contrat n'a plus de valeur par défaut, et les neuf valeurs sont rangées par ordre alphabétique. Un CDI pré-rempli est un type que personne n'a choisi, alors qu'il commande les règles de durée et la déclaration ; et le classement par fréquence supposée fait chercher les huit autres. L'envoi des plannings par SMS disparaît, formulaire, dossier et colonne. Le drapeau recueillait un consentement pour un canal qui n'a jamais existé et qu'il n'est pas prévu de brancher. Un consentement conservé sans finalité est une donnée collectée sans base légale : la minimisation impose de la supprimer, pas de la garder au cas où. Une divergence assumée avec le produit audité : la case « ouvrir un contrat maintenant » reste. Il faut pouvoir créer un administrateur ou un manager invité sans lui inventer un contrat de travail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
791dd523eb |
Donner une équipe à tout établissement neuf
Un établissement naissait sans équipe. Le planning s'ordonnant par équipe, il n'avait alors aucune ligne où poser un créneau — même avec des salariés rattachés. L'écran annonçait « cet établissement n'a pas encore d'équipe », ce qui se lit « il n'y a personne », et rien ne disait où en créer une. Deux corrections. La création d'un établissement pose une première équipe, à l'installation comme depuis les réglages ; elle se renomme, et rien n'empêche d'en ajouter d'autres. Et le message du planning dit désormais où aller : créer l'équipe dans les réglages, puis rattacher les salariés depuis l'onglet « Planification et accès » de leur fiche. Les établissements déjà créés gardent leur situation : le correctif empêche d'en produire de nouveaux sans équipe, il ne rattrape pas l'existant. Une équipe s'ajoute en deux clics depuis Réglages · Établissements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef3dd5c027 |
Fermer les listes de pays et de départements
Nationalité, pays de naissance, département de naissance et pays de l'adresse étaient des champs libres. « France », « FRANCE », « Française », « FR » et « france » désignent la même chose et se retrouvaient toutes en base. Le registre unique du personnel les imprime telles quelles, et un contrôle y lit cinq pays. Le code ISO 3166-1 est désormais stocké, jamais le libellé : il ne change pas d'orthographe, tient dans deux caractères, et c'est lui que reprennent les déclarations sociales. Le libellé n'est plus qu'un affichage, et le registre imprime le gentilé résolu depuis le code. La liste des pays couvre ce qu'un commerce de détail français rencontre — Europe, pourtour méditerranéen, Afrique francophone, principaux pays d'origine relevés par l'INSEE. Elle n'est pas universelle et ne le prétend pas : un pays absent s'ajoute en une ligne, plutôt que de rouvrir la saisie libre pour tout le monde. La France est en tête du sélecteur, le reste par ordre alphabétique ; un classement par fréquence supposée aurait été deviné, et se lirait comme un jugement. Les départements portent leur code officiel, Corse comprise — 2A et 2B, jamais 20, qui n'existe plus depuis 1976 mais survit dans les saisies manuelles. Les trois départements du régime local d'Alsace-Moselle sont identifiés à part : deux jours fériés de plus et un régime de maintien de salaire distinct, ce qui change la planification d'un établissement à quinze kilomètres d'un autre. Un dossier saisi avant la liste porte encore un libellé en clair. Le sélecteur n'y trouve pas d'option, affiche « non renseigné », et la première correction du dossier écrit le code. La valeur n'est ni perdue en silence ni opposée à qui vient corriger un champ voisin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fab497acdc |
Détailler les pauses d'un créneau, et prévenir le salarié
Un créneau portait un total de pause. Deux pauses de vingt minutes et une coupure de deux heures s'écrivaient toutes deux « 120 », et rien ne permettait de les distinguer — ni à l'écran, ni au contrôle. Or elles ne se planifient pas de la même façon : la première satisfait la pause minimale au-delà de six heures, la seconde interroge l'amplitude de la journée. Chaque pause a désormais sa ligne : durée, début facultatif, libellé, et si elle est rémunérée. Le début reste facultatif à dessein — beaucoup de pauses se prennent « quand c'est calme », et imposer une heure inventerait une précision que le planning n'a pas et qu'un contrôle prendrait pour un engagement. Le créneau garde deux totaux dérivés, et la distinction porte de l'argent. `breakMinutes` reste ce qu'il était, la part non rémunérée déduite du temps de travail : la paie, les compteurs et l'export continuent de le lire sans changer d'un caractère. `paidBreakMinutes` s'ajoute à côté, et la règle de pause minimale regarde la somme des deux — sans quoi un créneau dont la pause de vingt minutes est payée passerait pour un créneau sans pause, et l'employeur le plus généreux serait le seul à recevoir une alerte. La migration reprend les créneaux existants : leur total devient une pause unique, non rémunérée. Laisser la table vide aurait fait disparaître de l'écran des pauses qui comptent pourtant toujours dans les heures payées. En modification, les pauses sont remplacées et non fusionnées : le formulaire porte l'état voulu au complet, et rapprocher ligne à ligne ferait survivre une pause qu'on vient de retirer. L'avis d'affectation part enfin, sous sa propre nature d'envoi. Il est différé à la fin de la transaction : un refus sur le dernier jour d'une répétition annule les autres, et personne ne doit être prévenu d'un créneau qui n'existe pas. La case est décochée par défaut, à l'inverse du produit audité — c'est la publication de la semaine qui prévient, et cette case sert au créneau ajouté après coup, le cas où le salarié ne verrait rien sans elle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aa7336d749 |
Répéter un créneau sur plusieurs jours
Le panneau créait un créneau, pour un jour. Or un même horaire se répète d'un jour à l'autre bien plus souvent qu'il ne varie : ressaisir cinq fois « 9 h–17 h, pause 30 » était le geste le plus répété de la semaine, et le plus exposé à la faute de frappe. Les sept jours sont désormais cochables. Le jour ouvert est coché et verrouillé — on ne crée pas ailleurs qu'ici depuis la case d'un jour donné — et voyage dans un champ caché, puisqu'une case désactivée n'est pas envoyée. La création est **tout ou rien**. Un jour refusé, pour période close ou chevauchement, annule les autres : une répétition à demi appliquée laisserait un planning que personne n'a voulu, et qu'il faudrait défaire à la main pour le refaire. Le contrôle de période porte donc sur tous les jours demandés avant la première écriture. Trois champs du modèle étaient inatteignables depuis l'écran : le nombre de repas, la note du créneau, et l'étiquette en modification. Cette dernière était un vrai défaut — une étiquette posée de travers se corrigeait en supprimant le créneau pour le refaire, ce qui perdait sa validation et son historique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
804e14bf04 |
Étoffer le référentiel des types d'absence
Cinq types d'amorce ne tiennent pas un registre. Un commerce de détail rencontre un accident de trajet, une mise à pied, un congé pour examen, une visite médicale — et faute de type, ces absences se saisissent en « congé sans solde », ce qui fausse à la fois le décompte et la paie. Vingt-quatre types désormais, relevés sur le sélecteur du produit audité et recoupés avec les rubriques du dossier de paie. Les drapeaux sont des valeurs d'amorce, modifiables depuis les réglages : la nature Sécurité sociale ne se discute pas, mais l'acquisition de congés pendant une absence dépend de la convention. « Repos hebdomadaire » figure au sélecteur audité et pas ici. C'est un repos légal, porté par le modèle `Rest`, pas une absence qui se demande — en faire un type d'absence laisserait un manager refuser une obligation. Les familles de couleur passent de cinq à neuf, et deviennent une liste fermée avec ses libellés. Le champ des réglages était libre : une clé fautive s'y enregistrait sans bruit et la grille affichait un gris inexplicable. Quatre familles s'ajoutent — famille, formation, jour férié, sanction. Une sanction se distingue d'une absence ordinaire : elle est subie, elle se conteste, elle ne se lit pas comme un congé. La couleur groupe, le libellé précise. Donner sa teinte à chacun des vingt-quatre types rendrait la grille illisible bien avant la fin de la liste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a0524c56d7 |
Sortir la démonstration du chemin d'installation
Une instance neuve passait par le seed pour être utilisable. Or le seed installe une fiction : « Maison Rivage », des salariés inventés, quatre semaines de planning, des absences. Sur une instance de travail, ces noms se confondent avec de vrais salariés dans l'annuaire et au registre du personnel. Le trou qu'il bouchait était réel : après les migrations, une instance avait le schéma et rien à quoi l'accrocher. Aucun type d'absence, donc aucune demande saisissable. Aucune étiquette, donc aucun créneau nommé. Aucune convention, donc un moteur de règles muet qui laisse passer une semaine de soixante heures sans rien dire. L'écran d'installation pose donc désormais les référentiels : les douze étiquettes, cinq types d'absence, la convention d'amorce IDCC 1517 avec l'origine de chacun de ses paramètres, les durées de conservation et les jours fériés des deux prochaines années. Ce ne sont pas des exemples mais des minima, tous modifiables ensuite depuis les réglages. Trois choses restent délibérément absentes. Aucun dimanche du maire : la liste vient d'un arrêté municipal, et en inventer rendrait opposable un quota que personne n'a accordé. Aucun code Silae sur les types d'absence : la correspondance appartient au dossier du client. Aucun salarié, aucun créneau, aucune absence. Le seed devient `prisma/seed-demo.ts`, réservé au harnais Playwright. `db:seed` disparaît au profit de `db:seed:demo` : il n'y a plus rien à semer pour démarrer, et un nom qui le dit vaut mieux qu'un commentaire qui l'explique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a916ff73e |
Retirer la barre latérale au profit d'onglets de section
Deux défauts la condamnaient. Elle rognait la largeur utile au moment précis où la grille de planning en manque le plus. Et elle donnait à la navigation deux grammaires : des onglets sur la fiche salarié, une liste verticale partout ailleurs. Une grammaire se retient, deux se cherchent. Les entrées de la section courante s'affichent donc en onglets, dans la même forme que la fiche salarié. Une entrée sans écran construit reste visible et mène à l'écran d'attente : la masquer ferait croire le produit plus étroit que ce qu'il vise, la laisser inerte ferait croire la navigation cassée. La colonne centrale passe en largeur fixe. Une largeur qui suit la fenêtre fait bouger les colonnes du planning d'un poste à l'autre : la même semaine ne se lit pas au même endroit sur deux écrans, et l'œil doit se réorienter à chaque ouverture. Une largeur arrêtée rend la page reconnaissable ; sur une fenêtre étroite, elle défile horizontalement plutôt que de comprimer la grille. À l'impression, cette largeur n'a plus de sens et déborderait de la feuille : elle y est relâchée, comme les autres largeurs minimales de la grille. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5f656d05af |
Corriger le bandeau de compteurs, et régler impression et productivité
Le bandeau de la grille affichait contrat, planifié, écart, dimanches et jours de repos. PLAN.md §7.4 en demande cinq autres : heures contractuelles, planifié, absences, écart, repos compensateur. La capture du produit audité le confirme. L'écart n'était pas cosmétique. Les dimanches travaillés et les jours de repos se lisent déjà sur la grille, colonne par colonne. Le repos compensateur, lui, ne se lit nulle part ailleurs : c'est une contrepartie due — au dimanche travaillé, au dépassement de contingent — et l'omettre du bandeau masquait la seule valeur qui dit qu'une dette existe. Le compteur RC n'était alimenté par rien : les repos posés n'étaient tout simplement pas chargés avec la semaine. Ils le sont, et seul le repos compensateur y entre — le repos hebdomadaire est un droit déjà pris, les additionner ferait disparaître la dette dans un total qui ne veut rien dire. Deux écrans de réglages complètent le lot. L'impression gouverne le planning affiché en salle : orientation, densité, totaux, dimanche, colonne d'émargement. Cette dernière porte un avertissement — un émargement papier ne vaut pas décompte du temps de travail, et le décompte reste celui de PlanFlow. L'objectif de productivité est nullable et non « zéro par défaut » : zéro est un objectif inatteignable, l'absence de valeur dit qu'aucun objectif n'a été fixé. Vider le champ efface donc l'objectif au lieu de le mettre à zéro. L'écran rappelle aussi ce que l'indicateur n'est pas : il éclaire une décision d'organisation, il ne mesure pas le travail d'une personne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d8060eb4a9 |
Ouvrir le compte et les préférences au paramétrage
Quatre écrans manquaient faute de spécification. Les captures du produit audité les fournissent : identité de l'entreprise, comportement des plannings, droits ouverts aux salariés, décompte de paie. Les préférences sont trois blocs typés, pas un sac de clés-valeurs. Ce n'est pas de la rigueur gratuite : `smoothOvertimeMonthly` change le calcul des heures supplémentaires, et un JSON libre rendrait indétectable une clé mal orthographiée — le réglage paraîtrait actif sans l'être. Chaque formulaire déclare les clés qu'il gouverne. Une case décochée n'arrive pas dans le FormData ; sans cette déclaration, enregistrer « Plannings » remettrait à faux tout « Droits », un formulaire partiel effaçant ce qu'il n'affiche pas. Trois réglages portent un avertissement en évidence parce qu'ils déplacent des heures et non des pixels. Le lissage mensuel des heures supplémentaires suppose un accord d'aménagement du temps de travail : le décompte hebdomadaire est le principe, et sans accord l'aménagement est inopposable. Les pauses rémunérées et l'inclusion des repos dans les heures normales changent la répartition entre heures normales et majorées. Un interrupteur qui coûte de l'argent ne doit pas se présenter comme un réglage d'affichage. La ligne de préférences est créée à la demande : son absence vaut « tous les défauts ». Semer une ligne à l'installation obligerait à migrer chaque compte au premier réglage ajouté. L'adresse va sur le compte et non sur l'établissement : le siège porte l'identité juridique et figure au registre du personnel, là où l'établissement porte le SIRET, le fuseau effectif et les plannings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8c12b08961 |
Rendre les politiques de RTT administrables
Le modèle existait depuis WP-06, l'écran d'embauche proposait déjà de rattacher un salarié à une politique — mais rien ne permettait d'en créer une. La liste déroulante était vide sur une instance neuve, et le droit à RTT restait inatteignable. Les deux actions de ligne sont celles relevées à l'audit : assigner des employés, archiver. Pas de suppression. Une politique a produit des acquisitions au registre des compteurs ; l'effacer rendrait ces écritures inexplicables, et un solde de RTT est un droit ouvert, pas un affichage. Archiver arrête la reconduction sans toucher au reste : les jours déjà acquis restent au registre, les affectations en place. C'est la distinction que demande le critère d'acceptation de WP-06 — une politique archivée cesse de reconduire, elle ne défait pas ce qu'elle a fait. Le début de période s'écrit « MM-JJ », sans millésime : une politique qui porterait une année complète devrait être réécrite chaque janvier. L'affectation passe par un upsert sur la clé composée : réassigner un salarié déjà couvert ne doit ni échouer ni créer un doublon d'acquisition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
db03768025 |
Poser les modèles de documents et le dictionnaire RGPD
Deux livrables de WP-10 manquaient, et le second n'existait nulle part. Les modèles de documents produisent attestations et courriers types en résolvant les variables du dossier. Le rendu vit dans le domaine, avec une règle qui n'est pas négociable : une variable citée sans valeur fait échouer la génération. Une attestation trouée est un document faux, pas un document incomplet — le blanc se remarque à la relecture une fois sur deux, et la pièce part signée. De même, une variable inconnue est refusée à l'enregistrement du modèle, par l'administrateur qui l'écrit, plutôt que découverte par le gestionnaire devant le salarié qui attend. Le corps reste de l'HTML rédigé par un administrateur ; ce sont les valeurs venues du dossier qui sont échappées à l'insertion. Un nom de famille importé d'un autre outil ne doit pas pouvoir ouvrir une balise. `Document.templateId` est en ON DELETE SET NULL : une pièce déjà remise ne disparaît pas parce qu'on a effacé le modèle qui l'a produite. L'écran RGPD livre le dictionnaire donnée → finalité → base légale → destinataire → durée qu'exige la matrice n° 14. Le dictionnaire vit dans le code parce qu'il décrit le schéma ; les durées viennent de RetentionPolicy parce qu'elles se paramètrent et s'auditent. Une catégorie sans durée déclarée est signalée en rouge : le plan interdit d'appliquer une durée par défaut. L'affectation des bases légales porte un avertissement explicite. C'est une qualification juridique, pas un réglage : elle doit être confirmée par le responsable de traitement avant d'être opposée à quiconque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5b60e7ddb6 |
Donner aux indicateurs RH leur profondeur temporelle
Le tableau de bord montrait un mois. Un effectif, un taux de rotation ou un absentéisme lus sur un seul mois ne disent rien : c'est la pente qui informe, et elle demande une série. Trois analyses sur douze mois glissants — effectifs, heures, absences — servies par une seule requête. C'est ce qui garantit que l'effectif moyen affiché en « effectifs » soit exactement le dénominateur du taux d'absentéisme affiché en « absences ». Trois calculs séparés finiraient par diverger d'un arrondi, et l'écart se plaide mal devant un CSE. Les mois se découpent en mémoire après une lecture par nature, pas en douze requêtes par indicateur : sur une année glissante, la seconde approche rend l'écran inutilisable. Le graphique est en HTML. Douze barres ne justifient pas 90 ko de dépendance, et la CSP interdit de toute façon toute origine tierce. La hauteur ne porte jamais l'information seule — chaque barre affiche sa valeur, et le tableau reste la source. Une valeur non calculable s'y dessine en gris plutôt qu'à zéro : « 0 % de rotation » sur un établissement vide est une affirmation fausse, pas une absence de mouvement. Le mois de chaque ligne mène à ses lignes sources. Un indicateur qu'on ne peut pas ouvrir ne se corrige pas, il se conteste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1519b6bf9d |
Ouvrir le journal d'activité à la lecture
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> |
||
|
|
e4be93a208 |
Compléter les vues de planning
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> |
||
|
|
98b289784a |
Séparer les absences par état
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> |
||
|
|
527ea2e78a |
Ouvrir les référentiels au paramétrage
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> |
||
|
|
06198ddbce |
Recueillir à l'embauche ce qu'exige le contrat
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> |
||
|
|
3bf2cbabff |
Éditer le registre unique du personnel
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> |
||
|
|
8808fcdb95 |
Retirer le préambule du registre de paramétrage
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> |
||
|
|
894e880198 |
Alléger l'annuaire
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> |
||
|
|
93724b9d66 |
Rendre le rattachement et le périmètre modifiables
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> |
||
|
|
91f3b240e1 |
Poser le contrat au moment de l'embauche
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> |
||
|
|
5a113de02f |
Donner à l'annuaire une recherche et des filtres
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> |
||
|
|
bb8f84696a |
Rendre le contrat vivant depuis sa fiche
`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> |
||
|
|
c6abbaadc5 |
Faire de la fiche salarié un dossier à onglets, et le rendre saisissable
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> |
||
|
|
61a273548b |
Lever la barrière d'enrôlement une fois les codes notés
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> |
||
|
|
b243df4772 |
Étendre la préservation des formulaires, et corriger la publication
Suite de PR #31. `PersistentForm` couvre désormais les formulaires où la saisie compte : ajout d'un salarié, d'un établissement, d'une équipe, d'une entrée de registre, demande d'absence, heures réelles, correspondances de paie, réglages d'envoi, conservation, dépôt de pièce. Le composant gère `select`, `textarea`, cases et boutons radio, et reçoit `resetAfter` : passé l'état retourné par l'action en cas de succès, le formulaire revient aux valeurs rendues par le serveur — vide pour un ajout, la valeur enregistrée pour un champ d'édition. C'est ce qui rend visible une normalisation faite côté serveur. Les champs cachés sont exclus. Ils ne portent jamais une saisie mais l'état de l'application, et les rétablir écrase ce que le serveur vient de renvoyer. Le cas s'est produit sur `expectedVersion`, le verrou optimiste d'une semaine de planning. En éprouvant cela, un défaut plus grave est apparu, antérieur et sans rapport avec la préservation : le formulaire de publication recevait tantôt l'action de publication, tantôt celle de dépublication selon l'état, et cessait de suivre le changement. Lecture de l'en-tête `Next-Action` sur trois envois consécutifs : 1er envoi 60b8097e (dépublier) → brouillon 2e envoi 6012d40f (publier) → publiée 3e envoi 6012d40f (publier) → publiée Le bouton affichait « Dépublier » et republiait, sans message d'erreur puisque l'action réellement exécutée réussissait. Une seule action reçoit maintenant l'intention, portée par le bouton déclencheur — un champ caché serait remis à sa valeur d'origine par la réinitialisation que React applique après chaque action, un bouton ne l'est jamais. Le test du planning fait désormais deux allers-retours dans le même chargement : le premier envoi d'une page emploie toujours la bonne action, si bien qu'un seul aller-retour laissait passer le défaut selon l'état où la semaine avait été trouvée. Vérifié : le test échoue sur l'ancien câblage, passe sur le nouveau. 450 tests unitaires et d'intégration, 77 de bout en bout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
727c05407a |
Écran de première installation
Les migrations posent le schéma et rien d'autre : une instance neuve n'a aucun compte et aucun utilisateur, donc personne ne peut se connecter. Le seed n'y remédie pas — il installe une démonstration et refuse de tourner en production, à raison. La première visite est désormais redirigée vers `/installation` : nom de l'entreprise, premier établissement avec son fuseau, et compte administrateur. L'écran crée le compte, le catalogue des capacités, les cinq rôles fournis, le propriétaire et son périmètre, puis ouvre la session par le chemin ordinaire — le rôle propriétaire exige aussitôt un second facteur, comme il se doit. Il ne se rouvre pas. Une table `Installation` d'une seule ligne, contrainte en base et protégée par un trigger append-only, marque l'instance. Elle est délibérément hors RLS, et c'est sa raison d'être : la politique d'`Account` ne laisse voir que le compte courant, si bien qu'une instance installée paraîtrait vierge à qui n'a pas de session — et la création d'un propriétaire se rouvrirait à tout venant. Le recensement des politiques porte l'exception, affirmée dans les deux sens. Deux défauts trouvés en éprouvant l'écran sur une base réellement vierge : `INSERT ... RETURNING` sur `Account` était refusé. L'insertion est permise, mais la relecture de la ligne écrite passe par la politique de lecture, qui exige un compte courant. L'identifiant est donc tiré côté application et annoncé avant la création — la règle de partout, appliquée à la transaction qui crée le compte. Et un défaut qui dépassait cet écran : React 19 vide les champs non contrôlés dès qu'une action se termine, refus compris. Les champs vidés portant `required`, le clic suivant était arrêté par la validation du navigateur avant d'émettre un `submit` — le formulaire paraissait mort. Le formulaire de connexion en souffrait aussi ; la suite l'avait manqué parce qu'aucun test ne soumettait deux fois de suite. `PersistentForm` photographie la saisie à l'envoi et la rétablit, mots de passe exclus. Au passage, `Field` rattache son indication par `aria-describedby` : placée dans le `<label>`, elle entrait dans le nom accessible du champ. Éprouvé sur une base vierge, image de production, rôle NOSUPERUSER NOBYPASSRLS : redirection, trois refus motivés, installation, second facteur exigé, écran refermé pour le propriétaire comme pour un visiteur. 450 tests unitaires et d'intégration, 77 tests de bout en bout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
49618c57bf |
Faire poser le rôle applicatif par l'application elle-même
`db-init` suppose un orchestrateur qui honore `depends_on: service_completed_successfully`. Swarm l'ignore, et une pile déployée avant l'ajout du service ne le contient même pas. L'application redémarrait alors en boucle sur un refus d'authentification que le diagnostic ajouté précédemment décrivait sans que personne puisse le corriger. Elle le corrige donc elle-même au démarrage, si on lui confie les identifiants d'amorçage — et se tait sinon, pour ne pas contrarier un déploiement qui préfère les garder hors du conteneur applicatif. Le point d'entrée retire ces variables avant de lancer le serveur : le processus qui sert les requêtes ne les voit jamais. Le branchement create/alter se fait côté client et non dans un bloc `DO` : le corps d'un `DO` est une chaîne littérale, où `$1` n'est pas un paramètre de requête. L'échappement du mot de passe est confié à `quote_literal`, et le nom de rôle est refusé s'il n'a pas la forme d'un identifiant. Éprouvé sous authentification scram réelle : après réalignement, l'ancien mot de passe est refusé et le nouveau accepté, le rôle reste NOSUPERUSER NOBYPASSRLS et devient propriétaire de la base. Au passage, `withTenant` pose un budget de transaction explicite. Tout accès aux données passe par lui, si bien que le défaut Prisma de 5 s plafonnait en réalité chaque requête de l'application, et l'échec se présentait en `P2028` qui ne désigne ni la requête ni la cause. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
db43356919 |
Corriger la construction de l'image, pour de bon
`pnpm db:generate` échouait avant même le build : `prisma.config.ts` résolvait DATABASE_URL par `env()`, qui lève dès le **chargement** du fichier de configuration quand la variable manque. Or ce fichier est lu par toutes les commandes du CLI, y compris `generate`, qui ne se connecte à rien — la construction de l'image exigeait donc une base. Ma vérification précédente ne valait rien : j'avais éprouvé `pnpm build` seul, pas `pnpm db:generate && pnpm build`, la ligne qui échoue. Cette fois c'est la commande exacte du Dockerfile qui a été jouée, sans .env, et elle sort en 0. Corrigé au passage, du même genre que la troncature de l'écran de conservation : la liste des périodes de paie s'arrête aux 24 plus récentes sans le dire. Une période créée hors de cette fenêtre semblait ne pas s'être enregistrée. Le total est désormais rapporté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
495b91bf7f |
Rendre l'image constructible sans les secrets de production
La construction échouait : `src/lib/env.ts` validait le contrat d'environnement à l'import, et `src/server/db.ts` ouvrait la connexion Prisma de même. Next.js évalue les modules serveur pendant la construction, où ni la base ni la clé n'existent — l'image exigeait donc les secrets de production pour être bâtie, c'est-à-dire de les confier au constructeur. C'était à l'envers : ce sont des valeurs d'exécution. Les deux sont désormais résolus à la première lecture. La garantie ne bouge pas : le premier appel utile arrive bien avant qu'une requête aboutisse, et échoue avec le même message. Éprouvé en construisant sans aucune variable, ce que fait exactement Docker. Les tests décrivaient l'ancien contrat — ils restauraient l'environnement avant de lire, ce qui ne prouvait plus rien une fois la lecture différée. Réécrits pour lire pendant que les valeurs sont posées, avec deux cas de plus : l'import seul n'exige rien, et la lecture d'une configuration absente échoue. Ajout d'un `.dockerignore`, absent jusqu'ici. `.env` en tête, et ce n'est pas un détail : il porte la clé de chiffrement et les mots de passe de la base, et copié dans le contexte il aurait fini dans une couche de l'image, lisible par quiconque la récupère. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
e5524c6299 |
Produire la clé de chiffrement au premier démarrage
Le déploiement restait bloqué sur ENCRYPTION_KEY. J'ai tenu trop longtemps la position « pas de valeur par défaut », en confondant deux choses : refuser une clé livrée avec l'image — ce qui reste juste, une clé publiée dans un dépôt ne protège rien — et exiger qu'un humain en fabrique une avant tout démarrage. Une variable d'environnement n'est d'ailleurs pas un bon coffre : elle s'affiche dans `docker inspect` et dans l'interface de gestion. Un fichier produit au démarrage, dans un volume distinct de la base et des documents, n'est pas moins protégé — et une sauvegarde de l'un n'emporte plus la clé de l'autre. Le point d'entrée la produit donc si elle manque, l'écrit en 0600, et l'affiche une fois dans les journaux avec ce qu'il faut en faire. Une clé fournie explicitement l'emporte toujours : un déploiement qui gère ses secrets ailleurs ne doit pas être contrarié. La pile démarre désormais sans aucune variable. Éprouvé sur le script lui-même : première exécution, clé de 32 octets produite et annoncée ; deuxième, reprise en silence depuis le fichier ; avec une clé fournie, le fichier reste intact. Corrigé au passage, révélé par un échec transitoire du test : une écriture disque impossible — volume plein, droits, montage absent — remontait en exception non traitée. L'écran restait muet, la pièce n'était pas déposée et rien ne le disait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
ef2868b980 |
Rendre le déploiement Docker correct, et la RLS réellement active
Trois problèmes, dont un grave, trouvés en corrigeant un échec de déploiement. **Le compose connectait l'application en superutilisateur PostgreSQL.** Un superutilisateur contourne toute politique de sécurité au niveau ligne, y compris déclarée en FORCE : la seconde couche d'isolation était présente en base et absente des faits. Le README l'interdisait déjà noir sur blanc ; le chemin de déploiement que nous livrons faisait exactement l'inverse. Un script d'initialisation crée désormais un rôle `planflow_app` NOSUPERUSER NOBYPASSRLS, propriétaire de la base — il lui faut ce droit pour migrer, et les politiques sont en FORCE précisément pour s'appliquer aussi au propriétaire. Mesuré : en superutilisateur, deux lignes visibles sans compte courant ; avec le rôle dédié, zéro. Basculer la base de développement sur ce même rôle a révélé le défaut que le superutilisateur masquait : `resolveSession` lisait `Membership`, table filtrée par compte, sans périmètre. Avec la RLS active, plus personne ne pouvait se connecter. La résolution passe maintenant par une porte étroite — une politique qui n'ouvre que les lignes dont l'utilisateur est titulaire, sous `app.user_id` — le temps de trouver le compte, puis repasse par le périmètre ordinaire. La suite de tests traverse enfin la RLS au lieu de la contourner. **Les pièces du dossier salarié n'avaient aucun volume.** Elles étaient écrites dans la couche du conteneur et disparaissaient au premier redéploiement. Une pièce d'identité perdue ne se reconstitue pas. Le reste répond à la demande : Postgres préconfiguré — la base n'étant ni publiée ni attachée au réseau du proxy, ce mot de passe protège d'un conteneur voisin, pas d'Internet — réseau `nginx_default` déclaré externe avec l'application seule dessus, et port publié peu courant. ENCRYPTION_KEY reste la seule variable sans valeur par défaut, et n'en aura pas : elle chiffre le NIR, l'IBAN et les arrêts de travail. Une clé livrée avec l'image serait connue de quiconque lit ce dépôt. Au démarrage, l'application contrôle ses propres privilèges : elle refuse de se lancer si la base porte plus d'un compte, et se contente d'un avertissement s'il n'y en a qu'un — bloquer une installation mono-compte fermerait l'accès de l'entreprise à ses données pour une fuite entre clients qui ne peut pas se produire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
92a0ebf853 |
Configurer les rôles et leurs capacités
Critère d'acceptation de WP-01 resté sans écran : « un rôle personnalisé créé par un client modifie effectivement l'accès, sans changement de code ». Le catalogue de capacités existait, l'autorisation s'y adossait déjà, mais seul le semis pouvait attribuer quoi que ce soit. Deux dangers opposés, et il faut se protéger des deux. L'escalade d'abord : on ne peut accorder qu'une capacité qu'on détient soi-même, sans quoi la première personne autorisée à éditer un rôle s'accorde l'accès aux rémunérations en trois clics et le catalogue ne sert plus à rien. Le retrait, lui, reste permis même sur une capacité qu'on n'a pas — réduire un droit n'a jamais élargi le sien, et l'interdire empêcherait de corriger un rôle trop large. Le verrouillage ensuite : au moins un rôle doit conserver la gestion des droits. La condition se juge sur l'ensemble des rôles après coup, non sur celui qu'on édite — ce qui compte n'est pas que ce rôle-ci garde la capacité, mais qu'un rôle la garde. Sans cela une organisation se fermerait dehors, et le seul recours serait une intervention en base. Un rôle naît sans aucune capacité : en hériter de celles de son créateur distribuerait des droits que personne n'a demandés. Le niveau propriétaire se délègue à part. Un rôle système ne se supprime pas, un rôle attribué non plus — ses membres se retrouveraient dehors sans que personne l'ait décidé pour eux. Le test va jusqu'au bout : créer le rôle, l'attribuer, se connecter, constater qu'un écran s'ouvre et qu'un autre reste fermé. S'arrêter à « la case est cochée » n'aurait rien dit du critère. Trois défauts d'isolation des tests corrigés au passage, tous du même genre — un état qui s'accumule d'une exécution à l'autre : - la file des congés désignait « la première ligne de ce salarié » pour son nettoyage, et tombait sur une ligne laissée par un passage antérieur ; - l'écran de conservation tronquait à quarante pièces triées par ancienneté, faisant disparaître les pièces échues derrière celles dont il n'y a rien à dire — corrigé dans le produit, pas seulement dans le test ; - le test de verrouillage de période consommait un mois par exécution sans jamais le rendre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
6f3b630ced |
Déclarer et appliquer les durées de conservation
La table RetentionPolicy existait et cinq durées y étaient semées depuis la matrice ; rien ne les appliquait. Une durée déclarée que personne n'exécute est une conformité de papier. Aucune durée par défaut n'est appliquée, et c'est le point central : la matrice interdit explicitement d'aligner tout sur cinq ans. Un objet sans politique déclarée se conserve, et l'écran le signale plutôt que de le taire. Symétrie inverse, tout aussi importante : effacer faute de règle serait aussi fautif que garder indéfiniment. La justification est obligatoire au niveau du serveur. Une durée sans motif est une durée qu'on ne saura pas défendre le jour d'un contrôle. Les politiques sont effectif-datées comme le reste de l'application : une pièce déposée en mars relève de la règle en vigueur en mars. Sans cela, un durcissement rétroactif purgerait ce que la règle du moment autorisait à garder. La résolution va du précis au général — Document:SICK_NOTE avant Document — car un arrêt de travail et un contrat n'ont aucune raison de se conserver aussi longtemps. Trois refus distincts plutôt qu'un seul : absence de politique, conservation suspendue à titre probatoire, échéance non atteinte. Les confondre sous « rien à purger » empêcherait de vérifier que la conservation est réellement tenue. Un quatrième existe : employee_departure est déclaré mais non calculable, PlanFlow ne modélisant pas de date de départ — purger sur une date inventée serait pire que ne pas purger, et l'écran l'affiche comme tel. La purge s'exécute en ligne de commande pour une tâche planifiée, la matrice demandant des purges automatiques ; un bouton qu'il faut penser à presser n'en est pas une. Elle passe par le client scopé et la RLS, compte par compte. Les tables append-only en sont exclues par construction : le journal d'audit doit survivre aux données qu'il décrit, sans quoi on ne pourrait plus démontrer que la purge a eu lieu. L'échéance affichée est dérivée de la politique, jamais stockée — même discipline que la péremption d'un export. Deux pièges rencontrés : un objet de composants exporté depuis un module client ne survit pas au passage par un composant serveur, React n'en recevant qu'un undefined ; et le minLength du navigateur masquait le contrôle serveur de la justification, que des espaces suffisent à contourner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
29369d4a4e |
Déposer et consulter les pièces du dossier salarié
Rien ne pouvait être téléversé jusqu'ici : ni pièce d'identité, ni relevé d'identité bancaire, ni arrêt de travail. Le dossier RH n'existait qu'en champs de formulaire. Les fichiers vivent sur disque, chiffrés avec la clé qui protège déjà le NIR et l'IBAN. Le plan n'exige le chiffrement que des pièces jointes de santé ; les chiffrer toutes supprime une branche dont l'oubli serait silencieux et ne coûte rien de plus. Leur emplacement est tiré au sort, jamais dérivé du nom déposé : la traversée de chemin devient impossible par construction plutôt que par filtrage, et un filtre s'oublie. Le caractère sensible se déduit de la catégorie et n'est jamais saisi : laisser déclarer qu'un arrêt de travail n'est pas une donnée de santé reviendrait à laisser désactiver la journalisation de sa lecture. Cette lecture est inscrite au journal avant d'être servie, et l'écran l'annonce — celui qui ouvre la pièce doit savoir que sa consultation laisse une trace nominative. Les liens sont signés et durent deux minutes, comme l'exige le plan. La signature ne remplace pas le contrôle d'accès : la route revérifie session, capacité et périmètre. Elle s'y ajoute pour qu'un lien recopié dans un message cesse de fonctionner de lui-même, sans attendre qu'une session expire. La signature est éprouvée avant l'échéance, sans quoi répondre « expiré » à un lien fabriqué indiquerait qu'il aurait pu marcher. L'empreinte du clair est conservée et revérifiée à chaque lecture : servir un contenu qui ne correspond plus reviendrait à présenter comme authentique une pièce altérée. Retirer une pièce efface le contenu mais garde la ligne : le dossier doit conserver trace qu'elle a existé et qui l'a retirée. Aucune durée de conservation n'est appliquée par défaut — le plan l'interdit explicitement (§12.5). L'échéance reste nulle et l'écran le dit, plutôt que d'inventer « cinq ans partout ». Deux pièges d'outillage rencontrés et documentés dans les tests : le cookie de session étant marqué Secure, le client HTTP de Playwright ne l'émet pas sur http et faisait passer les refus pour de bonnes raisons sans rien prouver ; et la visionneuse PDF intégrée de Chromium ne restitue pas le corps d'une navigation, ce qui masquait la vérification d'intégrité du contenu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
2047d7f5cc |
Exiger un second facteur des rôles administrateur et RH
La matrice n° 15 l'impose ; les colonnes existaient en base depuis WP-01 mais rien ne les remplissait. TOTP (RFC 6238) plutôt qu'un code envoyé par message : le second facteur ne doit pas dépendre du canal qui sert déjà à réinitialiser le mot de passe, faute de quoi un accès à la boîte donnerait les deux d'un coup. L'algorithme est écrit ici plutôt qu'emprunté — trente lignes, et une dépendance de moins sur le chemin d'authentification. Il est éprouvé contre les vecteurs publiés de la RFC, ce qui distingue « le code change toutes les trente secondes » de « le code est celui qu'attend l'application du téléphone ». L'obligation est adossée aux capacités, pas à des rôles nommés qu'un client renomme librement : distribuer les droits, ou lire les rémunérations. Elle est tenue au point de passage de toutes les routes applicatives, où l'écran d'enrôlement remplace le contenu. Il remplace plutôt qu'il ne redirige : une redirection depuis un layout se joue aussi pendant la navigation qui suit la connexion, et Next y répond par une page vide. Le secret n'est enregistré qu'après qu'un code en a été tiré — l'enregistrer d'avance laisserait des comptes porteurs d'un facteur que leur détenteur ne sait pas produire, c'est-à-dire des comptes fermés. Le rejeu d'un code est refusé dans sa propre fenêtre : sans cela le facteur protège d'un mot de passe volé, pas d'un code lu par-dessus l'épaule. Dix codes de secours accompagnent chaque activation, conservés hachés. Et parce que PlanFlow est auto-hébergé et qu'il n'y a pas d'éditeur à appeler, un retrait depuis le serveur existe — l'accès « break glass » que demande la même ligne de la matrice. Sans lui, le second facteur deviendrait le risque principal plutôt que la protection. Un défaut trouvé par les tests, et il aurait été grave : la réactualisation de la route après activation remplaçait l'écran par celui d'un compte déjà enrôlé, emportant les codes de secours avant que leur destinataire ait pu les noter. La suite de tests suit le produit : la mise en place enrôle réellement le compte de direction et calcule les codes comme le ferait un téléphone. Les écrans qui n'éprouvent pas l'authentification ont migré vers la session commune — deux connexions parallèles sur un même compte se heurtent au refus de rejeu, ce qui est le comportement voulu et non un défaut à contourner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
874c3fcd80 |
Ouvrir un accès à un salarié par invitation
Jusqu'ici personne ne pouvait entrer dans l'application : le modèle Invitation existait en base mais aucun code ne s'en servait, et le seul moyen d'obtenir un compte était le jeu de données de démonstration. Le lien vaut autant qu'un mot de passe le temps de sa validité, d'où trois règles : sept jours, un seul usage, et une révocation possible sans attendre l'expiration. Seule l'empreinte du jeton est conservée — renvoyer une invitation émet donc un nouveau lien et invalide le précédent, ce qui est aussi la bonne réponse à « il a perdu le message ». Deux liens vivants pour un même accès, ce sont deux portes dont une seule est tracée comme ayant servi. Le compte est porté par le lien lui-même, préfixé au secret. La table est protégée par RLS, laquelle exige de connaître le compte avant toute lecture : sans ce préfixe il aurait fallu ouvrir la politique aux requêtes sans compte — c'est-à-dire la vider de son sens. Divulguer un identifiant opaque à qui est membre du compte ne coûte rien. Le lien est aussi rendu une fois, à l'écran de celui qui l'émet. Sans cela un déploiement neuf ne peut inviter personne : configurer le serveur d'envoi demande d'être connecté, et être connecté demande une invitation. Il n'est ni conservé ni journalisé. Deux cas à l'acceptation, un seul demande un mot de passe. Si aucun compte n'existe pour l'adresse, il est créé ; s'il en existe un, le salarié est rattaché sans qu'on touche à son mot de passe — détenir le lien prouve l'accès à la boîte, ce qui suffit à rattacher un accès mais ne justifie pas de réinitialiser l'authentification d'un compte existant. Pas de connexion automatique non plus : un mot de passe qu'on vient de choisir se fixe en s'en servant. `members.invite` est une capacité distincte de `members.edit` : ouvrir un accès n'est pas modifier un dossier, et tel client voudra confier l'un sans l'autre. Deux défauts trouvés par les tests plutôt qu'en production : le contrôle qui refuse un mot de passe contenant le nom du salarié laissait passer « riviere » pour « Rivière » faute de replier les accents — soit exactement la variante qu'on tape au clavier ; et les contextes « visiteur » des tests héritaient de la session du responsable, si bien que le parcours anonyme n'était pas éprouvé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
c3f7299f6c |
Configurer le serveur d'envoi de courrier
Sans serveur SMTP, PlanFlow ne peut ni inviter un salarié, ni notifier une publication de planning, ni délivrer l'information due au retour d'un arrêt. L'application n'expédie rien par elle-même : elle se connecte au serveur du client, pour que les messages partent de son domaine et que les salariés reconnaissent l'expéditeur. Le mot de passe SMTP est traité comme le NIR et l'IBAN — chiffré au repos avec la même clé, jamais renvoyé à l'écran (la page ne le charge même pas chiffré), jamais recopié dans un message d'erreur ni dans le journal d'audit. Les erreurs SMTP passent par une passe de masquage : les serveurs renvoient volontiers la commande AUTH en clair. Laisser le champ vide conserve le mot de passe enregistré, sans quoi changer un numéro de port casserait l'envoi. Enregistrer et éprouver sont deux gestes distincts : toute modification remet le réglage en « non vérifié », car un réglage non éprouvé n'est pas un réglage, c'est une intention. L'envoi de test vérifie d'abord la connexion — ce qui sépare une adresse de serveur fautive d'un mot de passe faux — puis expédie réellement. Les messages sont rendus en texte et en HTML depuis le même contenu, sans image, script ni ressource distante : la charte de télémétrie vaut aussi pour le courrier, un pixel de suivi dans un message RH est une collecte que personne n'a acceptée. Le journal d'envoi retient le destinataire et l'issue, pas le corps du message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
10a9c923e2 |
WP-09 : tableau de bord RH, et disparition des données de démonstration
`src/lib/demo` n'existe plus. Aucun écran de PlanFlow ne lit désormais autre chose que la base. Un indicateur doit être explicable Chaque tuile est un lien vers ses lignes sources : profils incomplets, fins de période d'essai, titres de séjour, entrées, sorties, avenants, journal des absences. Un chiffre qu'on ne peut pas ouvrir ne se corrige pas — il se conteste. Et chaque ligne mène à la fiche du salarié. Les manques sont **nommés**, pas comptés : « 6 profils incomplets » n'aide personne à agir, « il manque l'IBAN de trois salariés » se règle en un message. Le NIR et l'IBAN sont contrôlés par la présence de leur colonne chiffrée, jamais déchiffrés — savoir qu'une valeur existe n'exige pas de la lire. Des chiffres qui refusent de mentir - La rotation moyenne entrées et sorties : compter seulement les départs sous-estime la rotation d'une équipe qui recrute autant qu'elle perd. - Sur un effectif nul, elle rend `—` et non « 0 % ». Zéro pour cent de rotation sur un établissement vide est une affirmation fausse, pas une absence de mouvement. - L'absentéisme se rapporte aux jours **théoriquement travaillés**, pas aux jours calendaires : rapporter à 30 jours ferait passer un problème réel pour du bruit. - Un taux horaire absent vaut zéro dans le coût, jamais une estimation : afficher un coût inventé serait pire qu'un coût partiel. Les échéances remontent avant de tomber Périodes d'essai à 45 jours, titres de séjour à 90. Les échéances **dépassées** sont conservées et placées en tête : une période d'essai qu'on a laissé filer est plus urgente qu'une échéance à venir, et la masquer parce qu'elle est passée est précisément ce qui la rend coûteuse. Périmètre et confidentialité Le filtrage par établissement s'applique **avant** l'agrégation : les mouvements d'un autre établissement ne transparaissent pas, même fondus dans un total. Le journal des absences ne porte jamais le motif médical — un tableau de bord n'en a aucun besoin. Un salarié, qui n'a pas accès à l'annuaire, reçoit un accueil adapté plutôt qu'une erreur d'autorisation ou un tableau de bord vide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
2e9a4c5efe |
WP-07 : heures prévu/réalisé/payé et périodes de paie verrouillables
Les trois grandeurs Sans pointeuse, le réalisé est saisi par le manager. La matrice impose de distinguer trois grandeurs et non deux, avec deux règles qui ne se négocient pas : - **Sans heures réelles, le prévu fait foi.** Attendre une saisie qui ne viendra pas ne produirait aucune paie. Une saisie partielle — un début sans fin — ne suffit pas : elle donnerait une durée fantaisiste. - **Le paiement ne dépend jamais de la validation.** Une ligne non validée part en paie sur la base du réalisé. Bloquer le paiement d'heures accomplies faute de validation est précisément ce que la matrice interdit ; l'écran l'énonce pour qu'aucun manager ne croie l'inverse. Toute correction d'heures déjà saisies exige un motif — sans lui, une correction est indistinguable d'une erreur de saisie. Une première saisie n'en demande pas : il n'y a rien à corriger, et exiger un motif à chaque ligne le ferait remplir machinalement. Le verrou de période - Verrouiller fige les instantanés **et** ferme le mois : plus aucun créneau, aucune absence, aucune heure réelle ne peut être écrit sur ces dates. Le contrôle passe avant l'écriture, dans planning, absences et heures. - Déverrouiller exige une justification. Rouvrir périme les fichiers déjà transmis au cabinet, et six mois plus tard personne ne saurait pourquoi. - La **péremption d'un export est déduite** de `generatedAt < unlockedAt`, jamais stockée : la stocker obligerait à réécrire une trace qui doit rester append-only, et une trace réécrite ne prouve plus rien. - Supprimer une période demande de **retaper son libellé** : un « êtes-vous sûr » se clique sans lire, et la suppression efface des instantanés sur lesquels un export a pu être bâti. Un seul calcul pour trois écrans Les instantanés viennent de `buildPayrollPeriod`, la même fonction que le rapport de paie et l'export. C'est ce qui satisfait le critère croisé : trois calculs séparés divergeraient, et l'écart ne se verrait qu'au bulletin. Un test d'intégration compare instantané et rapport sur le même jeu de données. Tests enfin idempotents La suite e2e échouait au second passage : les absences acceptées d'une exécution bloquaient les demandes de la suivante. Trois corrections, dans l'ordre d'importance — chaque test travaille sur **son** salarié (le chevauchement se juge par personne), chaque test libère ce qu'il a créé, et les fenêtres de dates sont écartées d'une exécution à l'autre. Trois passages consécutifs passent désormais. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
0213375a8f |
WP-06 : absences, registre de compteurs, calendrier
Le dernier module de démonstration disparaît : `src/lib/demo/` ne contient plus que l'aperçu. Le décompte, là où l'on se trompe - **`endDate` est le dernier jour d'absence, pas la date de reprise.** C'est la confusion la plus fréquente du domaine : elle décompte un jour de trop ou de trop peu à chaque demande, et le salarié s'en aperçoit au solde, des mois plus tard. Le champ du formulaire s'appelle « Dernier jour d'absence », et un test couvre explicitement la confusion. - **Un jour férié dans un congé ne se décompte pas**, sans que l'utilisateur ait à scinder sa demande. Exiger la scission, c'est lui déplacer la charge d'un calcul que l'outil sait faire. - Le rythme du contrat est respecté : un temps partiel qui ne travaille jamais le mercredi ne consomme pas de congé ce jour-là. - Ouvrables ou ouvrés est un **paramètre**, pas une constante : se tromper fausse tous les soldes de la même façon. Jours fériés calculés, pas listés Une table écrite à la main est juste l'année où on l'écrit et fausse dès la suivante — et un férié manquant se décompte comme un jour de congé, sans que personne ne le voie. Les onze jours légaux sont donc calculés, Pâques comprise (algorithme grégorien anonyme, vérifié sur quatre années de référence). Quand aucun férié n'est enregistré pour l'année demandée, la demande passe mais l'écran le dit : mieux vaut l'annoncer que laisser croire à un décompte complet. Le registre est la source de vérité - Aucun solde n'est stocké : le solde est la **somme** des écritures. Un solde stocké se désynchronise, et la désynchronisation ne se voit qu'au moment où un salarié conteste. - `UPDATE` et `DELETE` sont refusés par un trigger PostgreSQL, pas seulement par l'application : une règle applicative finit par être contournée par un script de reprise. Les tests d'intégration écrivent directement en base pour le prouver. - Annuler une absence acceptée **contre-passe** la prise au lieu de l'effacer, à la date de la correction — antidater masquerait la correction dans les soldes déjà communiqués. `reversesId` est unique : contre-passer deux fois ferait repartir le solde dans l'autre sens. - Un ajustement manuel sans justification est refusé. Confidentialité du motif médical Un manager voit qu'un salarié est absent, pas de quoi il souffre (matrice n° 9). Le motif d'un arrêt n'est **pas chargé** pour qui n'a pas la capacité de le lire — un champ absent de la réponse ne peut fuiter ni par le HTML ni par un journal. Sur la grille de planning, l'absence s'affiche « Absence » : c'est suffisant pour ne pas planifier quelqu'un. Acquisition 2,5 jours ouvrables par mois travaillé ; 2 jours par mois d'arrêt maladie non professionnelle, plafonnés à 24 par an — droit issu de la réforme de 2024, dont l'oubli prive le salarié d'un droit acquis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
af9429901d |
WP-08 : export Silae, format relevé sur un export réel du dossier
Le format n'est plus déduit d'une documentation. Il est vérifié octet par octet sur un export du dossier (juillet 2026, 55 lignes) : un contrôle d'aller-retour a reproduit ce fichier **sans aucune ligne divergente**. Ce que le fichier réel a corrigé dans la spécification - L'export est en **ASCII pur**, pas en UTF-8 : aucun accent, y compris dans les libellés (« Heures travaillees », « Date debut »). Le sérialiseur translittère, pour qu'un salarié nommé « Rémi » n'introduise pas le premier octet non-ASCII du fichier. - Fins de ligne **CRLF**, la dernière comprise ; ni guillemet ni point-virgule final ; décimale point. - Heures avec au moins une décimale et au plus deux — `96.0`, `52.5`, `69.67` ; jours en entier nu — `14`, `22`. L'arrondi se fait **par ligne**, au centième : recomposer un total depuis les lignes peut donc s'écarter de quelques centièmes. C'est le comportement de l'export existant, et le reproduire est délibéré. - Un salarié sous contrat sans aucun créneau planifié figure quand même, avec sa durée contractuelle entière en heures manquantes. Ce que le fichier n'a pas dit Les codes sont maintenant connus — `AB-100`, `AB-200`, `AB-300`, `AB-630`, `EV-HDimanche`, `EV-HFerie`, `HS-HS25` — mais **savoir qu'un code existe ne dit pas ce qu'il désigne**. `EV-HDimanche` se lit ; `AB-300` non. Les premiers sont proposés, les seconds restent vides, et rien n'est confirmé d'office : l'export refuse de tourner tant que le gestionnaire de paie n'a pas validé chaque correspondance. C'est le signal d'arrêt de PLAN.md §8.2, maintenu. Refus plutôt que fichier partiel Un CSV incomplet se charge sans erreur dans Silae et rend la paie fausse pour les salariés qui en sont absents — l'échec est silencieux jusqu'au bulletin. L'export liste donc les manques et ne produit rien. Données personnelles L'export de référence porte les heures et les absences de salariés identifiables. Il **n'est pas versionné**, ni comme fixture ni comme donnée de démonstration. Ce sont les règles de forme qui sont figées dans les tests, avec les valeurs observées mais sans les matricules ni les volumes réels. Le fichier produit revient dans la réponse de l'action plutôt que par une URL : un fichier de paie ne doit pas rester adressable, mis en cache ou présent dans un historique de navigation. En base, seule l'empreinte est conservée — elle suffit à prouver qu'un réexport est identique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
44b5163713 |
WP-05 : moteur de règles de convention, effectif-daté
Le planning cesse d'être un tableur : il est confronté aux durées légales et conventionnelles à chaque écriture. Domaine — dix-huit règles pures - `src/domain/compliance/` : chaque règle est une fonction pure, testée à sa borne exacte. La valeur limite passe, un cran au-delà déclenche — c'est la seule forme de test qui protège d'une inégalité écrite à l'envers, et une inégalité à l'envers sur un repos quotidien est une infraction que personne ne verra. - 58 tests de règles, 14 sur les tranches d'heures : 43 h donnent 8 h à +25 %, 45 h donnent 8 h à +25 % et 2 h à +50 %. Aucune valeur dans le code - Les seuils vivent en base (`CollectiveAgreement.parameters`), validés par un schéma Zod qui refuse un jeu amputé : un seuil manquant lu comme `undefined` désactiverait silencieusement une règle de sécurité. - Un test charge deux jeux différents et vérifie que les résultats diffèrent. - `MAX_DAILY_AMPLITUDE` reste muette : l'IDCC 1517 ne fixe pas d'amplitude quotidienne. Inventer une borne ferait désactiver l'ensemble des alertes par le premier manager excédé. Effectif-datage — exigence n° 1 de la matrice - Les versions de convention coexistent ; un trigger PostgreSQL refuse de réécrire le contenu d'une version publiée, tout en laissant enregistrer une approbation postérieure. - Chaque constat mémorise la version appliquée. Un test d'intégration pose deux versions et vérifie qu'une semaine de mars n'est pas jugée sur la règle publiée en juillet. Dimanche — la double contrepartie Le taux de 100 % ne vient pas de la convention : l'IDCC 1517 n'en fixe aucun, et l'entreprise n'a pas d'accord. Il vient de l'article L3132-27, qui impose la rémunération doublée **et** un repos compensateur d'égale durée. Le moteur produit les deux ; un test échoue si l'un manque. Le quota des douze dimanches du maire est opposable, avec la liste arrêtée par établissement. Restitution - Les bloquants — chevauchement, créneau pendant une absence — annulent la transaction : mieux vaut refuser une saisie que garder un planning dont les heures se comptent deux fois. - Les avertissements se franchissent, avec un motif enregistré sur chaque constat et une entrée d'audit. Le constat acquitté reste affiché avec sa justification : le faire disparaître donnerait l'illusion qu'il a été résolu alors qu'il a été assumé. - Une modification revalide la semaine **et ses voisines** : le repos entre dimanche soir et lundi matin appartient à deux semaines. Règles de mineurs — non implémentées, volontairement La matrice ne couvre que les majeurs et le dossier n'a aucune source primaire sur les moins de 18 ans. Les codes sont réservés, les seuils absents. Les inventer donnerait une fausse assurance sur la population que le droit protège le plus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |
||
|
|
d9f55f866f |
WP-04 (suite) : vues par poste et par mois, duplication, édition, impression
Édition
- Le panneau de créneau sert aussi à la modification : déplacer un créneau,
c'est changer son jour, son salarié ou ses heures — exactement les champs de
la création. Deux écrans distincts finiraient par diverger.
- Duplication d'une semaine vers une autre, refusée si la destination contient
déjà des créneaux. Le report se fait **par heure locale**, pas par décalage de
sept jours : entre mars et avril, sept jours d'écart ne redonnent pas la même
heure.
Vues
- `/planning/etiquettes` : la semaine par poste. C'est la question du
responsable d'ouverture — « la caisse est-elle tenue samedi ? » — illisible
sur une grille dont les lignes sont des personnes. Les postes sans créneau
restent affichés : un poste vide toute la semaine est une information.
- `/planning/mois` : les heures par salarié et par jour, avec les journées de
plus de 10 h signalées. Vue de contrôle, pas d'édition.
- Les deux lisent `Shift` directement. Un test e2e pose un créneau et le
retrouve identique dans les quatre vues — c'est ce qui empêche l'une d'elles
de dériver vers son propre calcul.
Impression
- Feuille d'impression A4 paysage : les zones à défilement se déplient, les
lignes ne se coupent pas entre deux pages, et les aplats de poste sortent
(`print-color-adjust: exact`) sans être nécessaires à la lecture, puisque le
code du poste est imprimé dans chaque bloc.
- Pas de moteur PDF côté serveur : le navigateur sait paginer et enregistrer en
PDF. Un second moteur de rendu serait un second endroit où la grille peut
diverger de ce qui est à l'écran.
Accessibilité
- La grille porte enfin ses rôles ARIA (`grid`, `row`, `columnheader`,
`rowheader`, `gridcell`). Elle est faite de div pour la mise en page, mais se
lit comme un tableau : un lecteur d'écran doit pouvoir annoncer « ligne
Camille Ferrand, colonne mercredi ».
Cette structure a d'ailleurs révélé un test faux : `locator('div')` remontait au
conteneur de la grille, et le créneau du test atterrissait sur le premier
salarié de l'équipe au lieu de celui visé — une erreur invisible à l'écran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
|
||
|
|
023f236881 |
WP-04: planning sur données réelles — grille semaine, vue jour, publication par équipe
Le planning quitte le module de démonstration : la grille lit `WeeklySchedule` et `Shift`, et les quatre derniers fichiers de `src/lib/demo/` qui portaient des salariés fictifs disparaissent. Modèle - `TeamMember` : rattachement d'un salarié à une équipe, distinct de `MembershipScope` (qui dit ce qu'un manager a le droit de voir). Sans lui, un salarié sans créneau n'apparaîtrait pas dans la grille — c'est-à-dire l'état de départ de toute semaine en construction. - RLS sur `WeeklySchedule`, `Shift`, `Rest`, `DailyNote` et `TeamMember`. Temps - `src/domain/planning/week.ts` : repérage par couple année ISO + semaine ISO, jamais par date de début. Le lundi 29 décembre 2025 appartient à la semaine 1 de 2026 ; une clé fondée sur la date ferait apparaître deux semaines 1. - `zonedInstant` / `zonedMidnight` corrigent le décalage mesuré **à l'instant visé**. Le 29 mars 2026, minuit est en UTC+1 et 09 h en UTC+2 : ajouter neuf heures à minuit donnerait 10 h locales. - La semaine du retour à l'heure d'hiver dure 169 h, celle du passage à l'heure d'été 167 — vérifié par test. Écritures - Création, modification, suppression de créneau ; publication et dépublication **par équipe**, avec verrou optimiste sur `version` : deux managers sur la même grille est le cas normal, pas l'exception. - Chevauchement refusé en transaction, pas seulement dans le formulaire. - Modifier une semaine publiée exige `planning.edit_published`, capacité que le rôle manager n'a pas : un salarié a organisé sa semaine sur ce qu'il a lu. - Toute mutation laisse une entrée d'audit ; la suppression écrit sa trace **avant** l'effacement, sinon l'état supprimé serait perdu. Lecture - `planning.view_unpublished` filtre en base : sans cette capacité, les brouillons ne sont pas chargés du tout. Un test vérifie que les horaires n'apparaissent pas dans le HTML servi — un masquage CSS les y laisserait. - Vue jour reconstruite sur les mêmes données, amplitude déduite de la journée réelle plutôt que figée à 06 h–21 h. Vérification - 26 tests unitaires sur le repérage des semaines et la mise en grille. - Parcours e2e : poser un créneau, refus de chevauchement, publier, dépublier ; et ce que voient un salarié et un manager sur la même semaine. - `scripts/dev-db.sh` : la base de développement est éphémère dans cet environnement, la remonter ne doit pas être une redécouverte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv |