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
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
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
Adds the data model for accounts, locations, teams, users, memberships
and scopes, plus roles, the 70-capability catalogue, database-backed
sessions, and the audit log.
Isolation is enforced twice, independently. A Prisma extension injects
accountId into every query, and PostgreSQL row-level security filters
underneath it, keyed on a transaction-local setting. The first alone
leaves raw queries unguarded; the second alone returns empty results
without saying why.
Integration tests prove both against a real database rather than
through the application layer, which would only prove the application
layer. They create a restricted role to do it — and that exposed a trap
worth naming: **a PostgreSQL superuser bypasses row-level security even
with FORCE**. Connecting the app as one silently disables the second
layer while every application test still passes. checkTenantIsolation
now refuses to start in production on such a database, warns in
development, and reports through /api/sante. The README explains the
role to create.
The audit log is append-only by trigger, so it resists even a
superuser: a trail that can be rewritten proves nothing. Entries
carrying an adjustment or an unlock are rejected without a
justification, and known secret-bearing fields are redacted before
writing — the log is read, exported and kept for years, so it must not
become a second unencrypted copy of what is encrypted elsewhere.
Sensitive columns use AES-256-GCM with the key held outside the
database. Sign-in verifies a dummy hash for unknown accounts so timing
does not enumerate addresses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv