Commit Graph
9 Commits
Author SHA1 Message Date
Claude 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
2026-08-08 07:58:34 +00:00
Claude 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
2026-08-08 07:38:39 +00:00
Claude 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
2026-08-08 07:20:36 +00:00
Claude 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
2026-08-07 23:43:48 +00:00
Claude 8a314991d3 WP-03: employee records and contracts on real data
Replaces the demo module behind the team directory and the employee
record with scoped queries, and adds contracts, amendments, work
permits and the forfait-jours fields.

Writing the end-to-end test exposed a modelling error worth naming:
first and last names lived only on User, so an employee without an
application account had no name at all — the directory rendered
"— Salarié E0007". Most sales staff never sign in, and the personnel
register requires their name, so the name belongs to the record, not
to the login. Moved to EmployeeProfile with a data migration that
carries the existing names down from User.

Contract rules are pure functions tested at the boundaries. The case
that matters is an open-ended contract: a CDI with no end date overlaps
every later period, which a naive comparison of two date pairs misses,
and two overlapping active contracts would count one employee twice in
payroll. The check runs inside the transaction, not only in the form.

Forfait jours is refused without a written individual agreement and a
dated employee consent: without them the arrangement is unenforceable,
and enabling it would also switch off every weekly-duration control.

Salary and bank details are not merely hidden when the capability is
missing — they are never loaded. A field absent from the response
cannot leak through HTML, a log or an error message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 23:17:16 +00:00
Claude 92667b16b9 WP-02: locations, teams and the legal configuration register
Adds the referential models, the first database-backed settings
screens, and the register the compliance matrix requires before any
parameter is enforceable.

The register is the point of the lot. The matrix is explicit that
copying another product's configuration is not enough — each parameter
must carry its value, source, effective date, population and an
approver. Approval records the session's actor, never a form field: a
signature you can type yourself is worth nothing. The screen names the
domains that have no approved parameter yet, so the gap is visible
rather than assumed closed.

Two bugs of the same family, both now structurally impossible:

- The Prisma scoping extension read a hand-written list of models
  carrying accountId. The four models added here were missing from it,
  so writes failed with an opaque Prisma error — and a read would have
  silently returned every account's rows. The list is now derived from
  the schema itself.
- The RLS policies were likewise per-table. A new integration test
  fails if any table with an accountId column lacks forced RLS and both
  policies, which is the failure mode that hides best: nobody writes a
  wrong rule, someone forgets to write one.

An end-to-end test signs in as a manager and confirms the settings
screens refuse to render — the sidebar hiding them is a convenience,
the server check is the control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 23:04:26 +00:00
Claude 2c0e9e8dd4 Wire authentication into the application
Adds the sign-in screen, sign-out, and a server-side guard on every
application route. The guard lives in the layout rather than the proxy
because the proxy cannot query the database to check whether a session
was revoked — and revocation is the reason sessions are stored there.

Sign-in returns one message for an unknown account and for a wrong
password, and verifies a dummy hash when the account does not exist, so
neither the wording nor the timing enumerates staff addresses. An
end-to-end test compares the two messages rather than trusting the
code to keep them aligned.

The shell now shows the signed-in person and their role from the
database instead of hardcoded initials.

Playwright signs in once in a setup project and shares the cookie;
argon2 is deliberately slow, and logging in per test would also drive
the shared failed-attempt counter toward a lockout. The seed resets
that counter so repeated local runs cannot lock the demo account.

Two test locators had to be scoped to the form: Next's route announcer
carries role="alert" and an empty string, which silently satisfied the
assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 22:45:24 +00:00
Claude 93852c2602 Fix four defects the screenshots and tests exposed
Verifying the rendered pages rather than the build turned up real bugs:

- The WP-00 health page still sat at src/app/page.tsx and silently won
  the route over the new Aperçu screen, so the home page was a database
  status readout. Moved to /api/sante, where a probe belongs, and wired
  into the compose healthcheck.
- The unassigned row showed a +14 h delta against a contract of zero,
  reading as an overshoot when it is simply the volume left to staff.
  It now shows what there is to fill.
- Two sidebar entries lit at once: an anchor link matched its own page,
  and /equipe matched an employee record. Highlighting now resolves to
  the most specific match, and a test asserts exactly one entry lights
  per screen.
- Section tabs with no built screen pointed at the home page, which
  reads as a broken tab. They now lead to their first entry's
  placeholder.

Also gives truncated compliance alerts a title attribute, so a narrow
cell no longer says there is a problem without saying which.

Playwright can reuse a preinstalled browser through
PLAYWRIGHT_CHROMIUM_PATH when its revision differs from the bundled one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 22:26:38 +00:00
Claude dd639a86f5 WP-00: application foundation
Scaffolds the project: Next.js 16 App Router with strict TypeScript,
Prisma 7 on PostgreSQL 16, Tailwind 4, Vitest, Playwright, CI, and a
standalone Docker image that applies migrations on boot.

Makes the no-tracker rule of PLAN.md 3.7 enforceable rather than
stated. A per-request nonce-based CSP names no external origin, a unit
test fails if any network directive gains one, and a second test fails
if a tracking package appears in package.json. The end-to-end test
drives the standalone server the Docker image runs, not `next dev`,
so a proxy matcher that stopped matching could not pass unnoticed.

Environment is validated at import, so a missing DATABASE_URL fails at
boot with a readable message instead of surfacing later as a driver
error mid-export. ENCRYPTION_KEY is checked to be 32 bytes.

Three deviations from the plan, recorded in PLAN.md and README:
Next 16 rather than 15, `proxy.ts` rather than the now-deprecated
`middleware.ts`, and database-backed sessions rather than Auth.js v5,
which is still beta and whose JWTs would make the session revocation
required by compliance item 23 awkward.

Verified locally against PostgreSQL 16: migrations apply, extensions
created, typecheck, lint, 9 unit tests and the end-to-end header test
all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 17:54:11 +00:00