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
This commit is contained in:
27 files changed
+3373
-674
No files matched your search
@@ -66,11 +66,35 @@ model Team {
|
||||
|
||||
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
||||
scopes MembershipScope[]
|
||||
members TeamMember[]
|
||||
|
||||
@@index([accountId])
|
||||
@@index([locationId])
|
||||
}
|
||||
|
||||
/// Rattachement d'un salarié à une équipe.
|
||||
///
|
||||
/// Distinct de `MembershipScope`, qui dit ce qu'un manager a le droit de voir.
|
||||
/// Celui-ci dit où un salarié **apparaît dans la grille** — y compris une
|
||||
/// semaine où il n'a aucun créneau, ce qui est justement l'état de départ
|
||||
/// quand on construit un planning.
|
||||
model TeamMember {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
teamId String
|
||||
membershipId String
|
||||
/// Équipe d'affectation principale ; les autres sont des renforts.
|
||||
isPrimary Boolean @default(true)
|
||||
position Int @default(0)
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([teamId, membershipId])
|
||||
@@index([accountId])
|
||||
@@index([membershipId])
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Identité
|
||||
// ============================================================================
|
||||
@@ -122,6 +146,7 @@ model Membership {
|
||||
profile EmployeeProfile?
|
||||
contracts UserContract[]
|
||||
workPermits WorkPermit[]
|
||||
teams TeamMember[]
|
||||
|
||||
@@unique([accountId, employeeNumber])
|
||||
@@index([accountId])
|
||||
@@ -545,3 +570,106 @@ model WorkloadReview {
|
||||
@@index([accountId])
|
||||
@@index([userContractId])
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Planning — PLAN.md §4.4, WP-04
|
||||
// ============================================================================
|
||||
|
||||
/// Semaine de planning d'une équipe.
|
||||
///
|
||||
/// La publication est **par équipe** : l'audit montre un bouton « Dépublier »
|
||||
/// sur chaque section de la grille, pas un seul pour l'établissement.
|
||||
model WeeklySchedule {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
locationId String
|
||||
teamId String
|
||||
isoYear Int
|
||||
isoWeek Int
|
||||
status ScheduleStatus @default(DRAFT)
|
||||
publishedAt DateTime?
|
||||
publishedBy String?
|
||||
/// Verrou optimiste : la grille s'édite à plusieurs (PLAN.md §3.4).
|
||||
version Int @default(0)
|
||||
|
||||
shifts Shift[]
|
||||
rests Rest[]
|
||||
dailyNotes DailyNote[]
|
||||
|
||||
@@unique([teamId, isoYear, isoWeek])
|
||||
@@index([accountId])
|
||||
@@index([locationId, isoYear, isoWeek])
|
||||
}
|
||||
|
||||
enum ScheduleStatus {
|
||||
DRAFT
|
||||
VALIDATED
|
||||
PUBLISHED
|
||||
}
|
||||
|
||||
model Shift {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
weeklyScheduleId String
|
||||
/// Null = besoin non couvert, affiché sur sa propre ligne.
|
||||
membershipId String?
|
||||
|
||||
/// Regroupement dans la grille uniquement. Les durées se calculent depuis
|
||||
/// les instants — la nuit du changement d'heure, 22 h–06 h dure 7 h ou 9 h.
|
||||
localDate DateTime @db.Date
|
||||
startAt DateTime @db.Timestamptz
|
||||
endAt DateTime @db.Timestamptz
|
||||
|
||||
breakMinutes Int @default(0)
|
||||
actualStartAt DateTime? @db.Timestamptz
|
||||
actualEndAt DateTime? @db.Timestamptz
|
||||
actualBreakMinutes Int?
|
||||
|
||||
labelId String?
|
||||
mealCount Int @default(0)
|
||||
isValidated Boolean @default(false)
|
||||
validatedAt DateTime?
|
||||
validatedBy String?
|
||||
note String?
|
||||
version Int @default(0)
|
||||
|
||||
schedule WeeklySchedule @relation(fields: [weeklyScheduleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([accountId])
|
||||
@@index([weeklyScheduleId, localDate])
|
||||
@@index([membershipId, localDate])
|
||||
}
|
||||
|
||||
/// Repos de journée — hebdomadaire ou compensateur. Les pauses *dans* un
|
||||
/// créneau sont `Shift.breakMinutes`.
|
||||
model Rest {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
weeklyScheduleId String
|
||||
membershipId String
|
||||
localDate DateTime @db.Date
|
||||
restType RestType
|
||||
minutes Int?
|
||||
|
||||
schedule WeeklySchedule @relation(fields: [weeklyScheduleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([accountId])
|
||||
}
|
||||
|
||||
enum RestType {
|
||||
WEEKLY_REST
|
||||
COMPENSATORY_REST
|
||||
}
|
||||
|
||||
/// Ligne « Notes et événements » de la grille.
|
||||
model DailyNote {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
weeklyScheduleId String
|
||||
localDate DateTime @db.Date
|
||||
content String
|
||||
|
||||
schedule WeeklySchedule @relation(fields: [weeklyScheduleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([accountId])
|
||||
}
|
||||
Reference in new issue
Block a user