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
676 lines
20 KiB
Plaintext
676 lines
20 KiB
Plaintext
// PlanFlow — schéma de données. Voir PLAN.md §4.
|
||
//
|
||
// Prisma 7 : l'URL de connexion vit dans prisma.config.ts, plus ici.
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
// ============================================================================
|
||
// Tenancy — PLAN.md §4.1
|
||
// ============================================================================
|
||
|
||
model Account {
|
||
id String @id @default(cuid())
|
||
name String
|
||
siren String?
|
||
apeCode String?
|
||
collectiveAgreementId String?
|
||
/// Surcharges d'accord d'entreprise (PLAN.md §6.3). Vide aujourd'hui :
|
||
/// l'organisation auditée n'a pas d'accord d'entreprise.
|
||
agreementOverrides Json?
|
||
createdAt DateTime @default(now())
|
||
|
||
locations Location[]
|
||
memberships Membership[]
|
||
roles Role[]
|
||
auditLogs AuditLog[]
|
||
retention RetentionPolicy[]
|
||
featureFlags FeatureFlag[]
|
||
jobTitles JobTitle[]
|
||
labels Label[]
|
||
absenceTypes AbsenceType[]
|
||
legalConfig LegalConfigEntry[]
|
||
}
|
||
|
||
model Location {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
name String
|
||
siret String?
|
||
timezone String @default("Europe/Paris")
|
||
|
||
/// Taux moyen de cotisations patronales, en pourcentage.
|
||
employerContributionRate Decimal @default(0) @db.Decimal(5, 2)
|
||
silaeDossier String?
|
||
archivedAt DateTime?
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
teams Team[]
|
||
scopes MembershipScope[]
|
||
|
||
@@index([accountId])
|
||
}
|
||
|
||
model Team {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
locationId String
|
||
name String
|
||
position Int @default(0)
|
||
archivedAt DateTime?
|
||
|
||
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é
|
||
// ============================================================================
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
passwordHash String?
|
||
firstName String
|
||
lastName String
|
||
locale String @default("fr")
|
||
|
||
/// Deuxième facteur, exigé des rôles administrateur et RH (matrice n° 15).
|
||
mfaSecretEnc Bytes?
|
||
mfaEnrolledAt DateTime?
|
||
lastSignInAt DateTime?
|
||
failedAttempts Int @default(0)
|
||
lockedUntil DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
memberships Membership[]
|
||
sessions Session[]
|
||
}
|
||
|
||
/// Lien User ↔ Account. Porte le salarié : `userId` est nullable, car tous les
|
||
/// salariés ne se connectent pas — ils doivent rester plannifiables et
|
||
/// exportables sans compte (PLAN.md §4.1).
|
||
model Membership {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
userId String?
|
||
roleId String
|
||
lineManagerId String?
|
||
employeeNumber String
|
||
silaeMatricule String?
|
||
status MembershipStatus @default(INVITED)
|
||
invitedAt DateTime?
|
||
archivedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||
role Role @relation(fields: [roleId], references: [id])
|
||
lineManager Membership? @relation("LineManager", fields: [lineManagerId], references: [id], onDelete: SetNull)
|
||
reports Membership[] @relation("LineManager")
|
||
scopes MembershipScope[]
|
||
invitations Invitation[]
|
||
auditLogs AuditLog[]
|
||
profile EmployeeProfile?
|
||
contracts UserContract[]
|
||
workPermits WorkPermit[]
|
||
teams TeamMember[]
|
||
|
||
@@unique([accountId, employeeNumber])
|
||
@@index([accountId])
|
||
@@index([userId])
|
||
}
|
||
|
||
enum MembershipStatus {
|
||
INVITED
|
||
ACTIVE
|
||
ARCHIVED
|
||
}
|
||
|
||
/// Périmètre d'un membership. `allLocations` évite d'énumérer 34 établissements
|
||
/// pour un directeur — et de rater le 35ᵉ le jour de son ouverture.
|
||
model MembershipScope {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
membershipId String
|
||
allLocations Boolean @default(false)
|
||
locationId String?
|
||
teamId String?
|
||
|
||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||
location Location? @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([accountId])
|
||
@@index([membershipId])
|
||
}
|
||
|
||
/// Session serveur. En base plutôt qu'en JWT : la matrice n° 23 impose de
|
||
/// pouvoir révoquer une session, ce qu'un jeton signé ne permet pas.
|
||
model Session {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
tokenHash String @unique
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
lastSeenAt DateTime @default(now())
|
||
ip String?
|
||
userAgent String?
|
||
revokedAt DateTime?
|
||
revokedBy String?
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
model Invitation {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
membershipId String
|
||
tokenHash String @unique
|
||
email String
|
||
expiresAt DateTime
|
||
acceptedAt DateTime?
|
||
createdBy String
|
||
createdAt DateTime @default(now())
|
||
|
||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([accountId])
|
||
}
|
||
|
||
// ============================================================================
|
||
// Autorisation — PLAN.md §4.2 et §5
|
||
// ============================================================================
|
||
|
||
/// Rôle configurable par le client. Le code référence `key`, jamais `name` :
|
||
/// renommer « Manager » en « Responsable » ne doit rien casser.
|
||
model Role {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
key String
|
||
name String
|
||
isSystem Boolean @default(false)
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
permissions RolePermission[]
|
||
memberships Membership[]
|
||
|
||
@@unique([accountId, key])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Capacité stable, nommée `ressource.action.qualificatif`. Référentiel global :
|
||
/// les capacités sont définies par le produit, seule leur attribution varie.
|
||
model Permission {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
category String
|
||
label String
|
||
|
||
roles RolePermission[]
|
||
}
|
||
|
||
model RolePermission {
|
||
roleId String
|
||
permissionId String
|
||
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||
|
||
@@id([roleId, permissionId])
|
||
}
|
||
|
||
// ============================================================================
|
||
// Traçabilité — PLAN.md §3.5
|
||
// ============================================================================
|
||
|
||
/// Journal d'audit. Append-only, imposé par un trigger PostgreSQL : une piste
|
||
/// qu'on peut réécrire ne prouve rien.
|
||
model AuditLog {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
actorMembershipId String?
|
||
action String
|
||
entityType String
|
||
entityId String
|
||
before Json?
|
||
after Json?
|
||
reason String?
|
||
ip String?
|
||
userAgent String?
|
||
occurredAt DateTime @default(now())
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
actor Membership? @relation(fields: [actorMembershipId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([accountId, entityType, entityId])
|
||
@@index([accountId, occurredAt])
|
||
}
|
||
|
||
/// Durées de conservation par objet — PLAN.md §12.5.
|
||
/// « 5 ans partout » est explicitement proscrit : chaque durée porte son point
|
||
/// de départ et sa justification.
|
||
model RetentionPolicy {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
objectType String
|
||
durationMonths Int
|
||
startPoint String
|
||
justification String
|
||
legalHold Boolean @default(false)
|
||
effectiveFrom DateTime @db.Date
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, objectType, effectiveFrom])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Verrou de conformité — PLAN.md §12.4.
|
||
/// Une fonctionnalité de contrôle reste inactive tant que la notice au salarié
|
||
/// et l'avis du CSE ne sont pas enregistrés.
|
||
model FeatureFlag {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
key String
|
||
enabled Boolean @default(false)
|
||
noticeDocumentRef String?
|
||
noticeDeliveredAt DateTime?
|
||
cseOpinionAt DateTime?
|
||
activatedAt DateTime?
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, key])
|
||
}
|
||
|
||
// ============================================================================
|
||
// Référentiels — PLAN.md §4.3 et WP-02
|
||
// ============================================================================
|
||
|
||
/// Intitulé d'emploi. Distinct du poste de planning : l'emploi qualifie le
|
||
/// contrat, le poste qualifie une occupation dans la journée.
|
||
model JobTitle {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
name String
|
||
archivedAt DateTime?
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, name])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Étiquette de planning — le « poste » coloré de la grille.
|
||
model Label {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
code String
|
||
name String
|
||
/// Code de la palette catégorielle (voir src/lib/design/postes.ts).
|
||
paletteKey String
|
||
position Int @default(0)
|
||
archivedAt DateTime?
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, code])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Type d'absence. `isSocialSecurity` isole maladie, maternité et AT : le
|
||
/// journal des absences les filtre séparément, et ce sont des données de santé.
|
||
model AbsenceType {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
code String
|
||
name String
|
||
colorKey String
|
||
isPaid Boolean @default(true)
|
||
countsAsWorkTime Boolean @default(false)
|
||
affectsPaidLeaveAccrual Boolean @default(true)
|
||
isSocialSecurity Boolean @default(false)
|
||
requiresJustification Boolean @default(false)
|
||
minNoticeDays Int?
|
||
/// Partie <code> de AB-<code> à l'export Silae. Null tant qu'elle n'a pas
|
||
/// été fournie par le dossier du client (PLAN.md §8.2).
|
||
silaeCode String?
|
||
archivedAt DateTime?
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, code])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Registre de paramétrage juridique — PLAN.md §12.7.
|
||
///
|
||
/// La matrice impose de faire **signer** chaque paramètre avant migration, avec
|
||
/// sa valeur, sa source, sa date d'effet, sa population et son approbateur. Un
|
||
/// paramètre sans cette traçabilité n'est pas opposable : c'est ce registre qui
|
||
/// distingue une configuration justifiée d'une valeur recopiée d'un autre
|
||
/// logiciel.
|
||
model LegalConfigEntry {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
domain String
|
||
key String
|
||
value String
|
||
source String
|
||
effectiveFrom DateTime @db.Date
|
||
population String
|
||
approvedBy String?
|
||
approvedAt DateTime?
|
||
attachmentRef String?
|
||
createdAt DateTime @default(now())
|
||
|
||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([accountId, domain, key, effectiveFrom])
|
||
@@index([accountId])
|
||
}
|
||
|
||
// ============================================================================
|
||
// Dossier salarié et contrats — PLAN.md §4.3, WP-03
|
||
// ============================================================================
|
||
|
||
/// Dossier personnel. NIR, IBAN et BIC sont chiffrés au repos (PLAN.md §3.6) :
|
||
/// une sauvegarde volée ne doit pas suffire à les lire.
|
||
model EmployeeProfile {
|
||
membershipId String @id
|
||
accountId String
|
||
|
||
/// Nom d'état civil, porté par le dossier et non par le compte utilisateur :
|
||
/// la plupart des salariés n'ont pas de compte, et le registre unique du
|
||
/// personnel exige leur nom.
|
||
firstName String
|
||
lastName String
|
||
|
||
birthDate DateTime? @db.Date
|
||
birthPlace String?
|
||
nationality String?
|
||
addressLine1 String?
|
||
postalCode String?
|
||
city String?
|
||
country String?
|
||
phone String?
|
||
personalEmail String?
|
||
|
||
socialSecurityNumberEnc Bytes?
|
||
ibanEnc Bytes?
|
||
bicEnc Bytes?
|
||
|
||
emergencyContactName String?
|
||
emergencyContactPhone String?
|
||
|
||
updatedAt DateTime @updatedAt
|
||
|
||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Titre de séjour et son échéance. Le tableau de bord RH surveille les
|
||
/// expirations : un titre périmé interdit l'emploi.
|
||
model WorkPermit {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
membershipId String
|
||
permitType String
|
||
reference String
|
||
issuedAt DateTime? @db.Date
|
||
expiresAt DateTime @db.Date
|
||
|
||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([accountId])
|
||
@@index([membershipId])
|
||
}
|
||
|
||
model UserContract {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
membershipId String
|
||
locationId String
|
||
|
||
contractType ContractType
|
||
startDate DateTime @db.Date
|
||
endDate DateTime? @db.Date
|
||
trialEndDate DateTime? @db.Date
|
||
|
||
/// Organisation du temps. Le forfait jours exclut le décompte horaire (§6.4).
|
||
workTimeArrangement WorkTimeArrangement @default(HOURLY)
|
||
weeklyHours Decimal @default(35) @db.Decimal(5, 2)
|
||
forfaitDaysPerYear Decimal? @db.Decimal(5, 1)
|
||
/// Convention individuelle écrite. Sans elle le forfait est inopposable :
|
||
/// l'activation est refusée.
|
||
forfaitAgreementRef String?
|
||
forfaitAgreedAt DateTime?
|
||
isModulated Boolean @default(false)
|
||
|
||
hourlyRate Decimal? @db.Decimal(10, 4)
|
||
monthlySalary Decimal? @db.Decimal(10, 2)
|
||
jobTitleId String?
|
||
classification String?
|
||
coefficient String?
|
||
|
||
status ContractStatus @default(ACTIVE)
|
||
endReason String?
|
||
version Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
|
||
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
|
||
amendments Amendment[]
|
||
|
||
@@index([accountId])
|
||
@@index([membershipId])
|
||
}
|
||
|
||
/// Liste exhaustive relevée dans le filtre « Tous les types de contrats ».
|
||
enum ContractType {
|
||
APPRENTISSAGE
|
||
CDD
|
||
CDI
|
||
DIRIGEANT_ASSIMILE_SALARIE
|
||
DIRIGEANT_NON_SALARIE
|
||
EXTRA
|
||
INTERIM
|
||
STAGIAIRE
|
||
SAISONNIER
|
||
}
|
||
|
||
enum ContractStatus {
|
||
DRAFT
|
||
ACTIVE
|
||
ENDED
|
||
}
|
||
|
||
enum WorkTimeArrangement {
|
||
HOURLY
|
||
FORFAIT_JOURS
|
||
}
|
||
|
||
/// Avenant. Conserve l'historique plutôt que d'écraser le contrat : un contrôle
|
||
/// demande l'état du contrat au moment des faits, pas son état actuel.
|
||
model Amendment {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
userContractId String
|
||
effectiveDate DateTime @db.Date
|
||
changes Json
|
||
reason String?
|
||
createdBy String
|
||
createdAt DateTime @default(now())
|
||
|
||
contract UserContract @relation(fields: [userContractId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([accountId])
|
||
@@index([userContractId])
|
||
}
|
||
|
||
/// Décompte des jours d'un salarié au forfait. Conservation 3 ans (§12.5).
|
||
model ForfaitDayEntry {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
userContractId String
|
||
localDate DateTime @db.Date
|
||
quantity Decimal @db.Decimal(2, 1)
|
||
createdBy String
|
||
createdAt DateTime @default(now())
|
||
|
||
@@unique([userContractId, localDate])
|
||
@@index([accountId])
|
||
}
|
||
|
||
/// Entretien annuel de charge, obligatoire au forfait jours (matrice n° 7).
|
||
model WorkloadReview {
|
||
id String @id @default(cuid())
|
||
accountId String
|
||
userContractId String
|
||
heldAt DateTime @db.Date
|
||
summary String
|
||
actions String?
|
||
|
||
@@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])
|
||
}
|