mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- migration r2_exploitation (10 tables) : WorkOrder (référence séquentielle, horodatages), WorkOrderEvent, Request (1-1, motif de rejet), ReferenceValue, InterventionReport (6 FK), TaskTemplate/ChecklistItem, Meter/MeterReading - contrat : 15 opérations (41 total) ; la table des transitions et les champs requis du bilan vivent dans @siop/shared ; la fiche OT expose allowedTransitions + closureBlockers (messages métier) - API : machine à états stricte ; garde de clôture (bilan 3 champs requis + checklist sans tâche en attente) ; approbation → OT lié 1-1 (409 si déjà traitée) ; rejet à motif obligatoire ; scoping « voir autre » sur listes et accès directs (404 sans fuite) ; validation des valeurs de bilan par champ ; « personne bloquée » triée en tête côté API - seed : 31 valeurs de référentiels, 8 gabarits (parachute réglementaire), OT/demandes/compteurs de la maquette — idempotent - 45 tests verts (95 % stmts / 79 % branches) dont la recette officielle rejouée de bout en bout ; smoke test sur build de prod Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
353 lines
12 KiB
Plaintext
353 lines
12 KiB
Plaintext
// Modèle R0 — identité & permissions (docs/03-architecture/modele-donnees.md).
|
||
// La matrice rôles × objets × droits vit EN BASE ; le JWT ne porte jamais de droits.
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
// Cibles explicites : poste de dev (native) + conteneur node:24-slim
|
||
// (OpenSSL 3) en x64 (serveur partenaire) et arm64 (répétition locale).
|
||
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
model Role {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique // 7 rôles seedés — voir @siop/shared ROLE_NAMES
|
||
users User[]
|
||
permissions Permission[]
|
||
}
|
||
|
||
model Permission {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
roleId String @db.Uuid
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
objectCategory String // enum applicatif — voir @siop/shared OBJECT_CATEGORIES
|
||
canView Boolean @default(false)
|
||
canViewOther Boolean @default(false) // « voir autre » : au-delà de ses propres objets
|
||
canCreate Boolean @default(false)
|
||
canEdit Boolean @default(false)
|
||
canDelete Boolean @default(false)
|
||
|
||
@@unique([roleId, objectCategory])
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
email String @unique
|
||
displayName String
|
||
passwordHash String? // null tant que le compte n'est pas activé (R1)
|
||
phone String?
|
||
roleId String @db.Uuid
|
||
role Role @relation(fields: [roleId], references: [id])
|
||
isActive Boolean @default(true)
|
||
isDemo Boolean @default(false) // seul un compte isDemo est empruntable (ADR-002)
|
||
// Invitation (R1) : lien d'activation 7 jours, usage unique.
|
||
// Statut dérivé : invité = passwordHash null && token présent.
|
||
activationToken String? @unique
|
||
activationExpiresAt DateTime?
|
||
teams Team[]
|
||
// R2 — exploitation
|
||
workOrdersAssigned WorkOrder[] @relation("WorkOrderAssignees")
|
||
workOrdersCreated WorkOrder[] @relation("WorkOrderCreator")
|
||
workOrderEvents WorkOrderEvent[]
|
||
requests Request[]
|
||
checklistDone ChecklistItem[]
|
||
meterReadings MeterReading[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([roleId])
|
||
}
|
||
|
||
// ————— R1 — Référentiel (docs/03-architecture/modele-donnees.md §R1) —————
|
||
|
||
enum CategoryKind {
|
||
EQUIPMENT
|
||
COMPONENT_TYPE
|
||
}
|
||
|
||
enum AssetStatus {
|
||
IN_SERVICE
|
||
OUT_OF_SERVICE
|
||
UNDER_MAINTENANCE
|
||
}
|
||
|
||
model Category {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
kind CategoryKind
|
||
name String
|
||
isActive Boolean @default(true) // désactivable, jamais supprimée si utilisée
|
||
assets Asset[]
|
||
components AssetComponent[]
|
||
taskTemplates TaskTemplate[]
|
||
|
||
@@unique([kind, name])
|
||
}
|
||
|
||
model Location {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String
|
||
parentId String? @db.Uuid // site (null) → zone ; profondeur max 2 (service)
|
||
parent Location? @relation("LocationTree", fields: [parentId], references: [id])
|
||
children Location[] @relation("LocationTree")
|
||
address String?
|
||
city String?
|
||
guardianName String?
|
||
guardianPhone String?
|
||
latitude Float?
|
||
longitude Float?
|
||
// + colonne PostGIS générée (voir migration r1_referentiel) :
|
||
// position geography(Point,4326) GENERATED ALWAYS AS (…) STORED
|
||
assets Asset[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([parentId])
|
||
}
|
||
|
||
model Asset {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
reference String @unique // « A1 » — imprimée sur l'étiquette QR
|
||
brand String
|
||
model String?
|
||
serialNumber String?
|
||
commissionedAt DateTime?
|
||
loadKg Int?
|
||
floors Int?
|
||
status AssetStatus @default(IN_SERVICE) // statut d'ÉQUIPEMENT ≠ statut d'OT
|
||
categoryId String @db.Uuid
|
||
category Category @relation(fields: [categoryId], references: [id])
|
||
locationId String @db.Uuid
|
||
location Location @relation(fields: [locationId], references: [id])
|
||
components AssetComponent[]
|
||
workOrders WorkOrder[]
|
||
requests Request[]
|
||
meters Meter[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([locationId])
|
||
@@index([categoryId])
|
||
}
|
||
|
||
// Organe : PAS de colonne emplacement — « un organe n'a pas d'emplacement
|
||
// propre » est garanti par construction (décision maquettes R1).
|
||
model AssetComponent {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
assetId String @db.Uuid
|
||
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||
typeId String @db.Uuid
|
||
type Category @relation(fields: [typeId], references: [id])
|
||
designation String?
|
||
|
||
@@index([assetId])
|
||
}
|
||
|
||
model Team {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique
|
||
description String?
|
||
members User[]
|
||
}
|
||
|
||
// ————— R2 — Exploitation (docs/03-architecture/modele-donnees.md §R2) —————
|
||
|
||
enum WorkOrderType {
|
||
CORRECTIVE // Dépannage
|
||
PREVENTIVE // Maintenance (grille du mois)
|
||
WORKS // Travaux
|
||
}
|
||
|
||
enum WorkOrderStatus {
|
||
OPEN
|
||
IN_PROGRESS
|
||
ON_HOLD
|
||
DONE
|
||
CANCELLED
|
||
}
|
||
|
||
enum WorkOrderPriority {
|
||
NONE
|
||
LOW
|
||
MEDIUM
|
||
HIGH
|
||
PERSON_TRAPPED // personne bloquée — urgence absolue
|
||
}
|
||
|
||
enum RequestStatus {
|
||
RECEIVED
|
||
APPROVED
|
||
REJECTED
|
||
}
|
||
|
||
enum ChecklistState {
|
||
PENDING
|
||
DONE
|
||
NA
|
||
}
|
||
|
||
enum BilanField {
|
||
DOOR_STATE
|
||
CABIN_POSITION
|
||
ANOMALY
|
||
EXTERNAL_CAUSE
|
||
ACTION_TAKEN
|
||
COMPONENT_CONCERNED
|
||
}
|
||
|
||
enum MeterKind {
|
||
RUNNING_HOURS
|
||
STARTS
|
||
}
|
||
|
||
model WorkOrder {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
reference String @unique // OT-2026-0341
|
||
title String
|
||
description String?
|
||
type WorkOrderType
|
||
status WorkOrderStatus @default(OPEN) // machine à états stricte (service)
|
||
priority WorkOrderPriority @default(NONE)
|
||
assetId String @db.Uuid
|
||
asset Asset @relation(fields: [assetId], references: [id])
|
||
dueDate DateTime?
|
||
assignees User[] @relation("WorkOrderAssignees")
|
||
createdById String? @db.Uuid
|
||
createdBy User? @relation("WorkOrderCreator", fields: [createdById], references: [id])
|
||
startedAt DateTime?
|
||
completedAt DateTime?
|
||
cancelledAt DateTime?
|
||
events WorkOrderEvent[]
|
||
checklist ChecklistItem[]
|
||
report InterventionReport?
|
||
request Request?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([assetId])
|
||
@@index([status])
|
||
}
|
||
|
||
model WorkOrderEvent {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
workOrderId String @db.Uuid
|
||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||
kind String // COMMENT · STATUS_CHANGED · ASSIGNED · CREATED · FROM_REQUEST
|
||
message String?
|
||
byId String? @db.Uuid
|
||
by User? @relation(fields: [byId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([workOrderId])
|
||
}
|
||
|
||
model Request {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
reference String @unique // DEM-2026-0112
|
||
description String
|
||
isPersonTrapped Boolean @default(false)
|
||
status RequestStatus @default(RECEIVED)
|
||
rejectionReason String? // REQUIS au rejet (service)
|
||
assetId String @db.Uuid
|
||
asset Asset @relation(fields: [assetId], references: [id])
|
||
requestedById String? @db.Uuid
|
||
requestedBy User? @relation(fields: [requestedById], references: [id])
|
||
requesterName String? // portail public via QR (R2.4)
|
||
workOrderId String? @unique @db.Uuid // lien 1-1 — jamais de doublon
|
||
workOrder WorkOrder? @relation(fields: [workOrderId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([assetId])
|
||
}
|
||
|
||
// Référentiels administrables du bilan codé (un par champ)
|
||
model ReferenceValue {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
field BilanField
|
||
label String
|
||
isActive Boolean @default(true)
|
||
|
||
doorStates InterventionReport[] @relation("BilanDoorState")
|
||
cabinPositions InterventionReport[] @relation("BilanCabinPosition")
|
||
anomalies InterventionReport[] @relation("BilanAnomaly")
|
||
externalCauses InterventionReport[] @relation("BilanExternalCause")
|
||
actionsTaken InterventionReport[] @relation("BilanActionTaken")
|
||
componentsConcerned InterventionReport[] @relation("BilanComponentConcerned")
|
||
|
||
@@unique([field, label])
|
||
}
|
||
|
||
model InterventionReport {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
workOrderId String @unique @db.Uuid
|
||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||
note String?
|
||
|
||
doorStateId String? @db.Uuid
|
||
doorState ReferenceValue? @relation("BilanDoorState", fields: [doorStateId], references: [id])
|
||
cabinPositionId String? @db.Uuid
|
||
cabinPosition ReferenceValue? @relation("BilanCabinPosition", fields: [cabinPositionId], references: [id])
|
||
anomalyId String? @db.Uuid
|
||
anomaly ReferenceValue? @relation("BilanAnomaly", fields: [anomalyId], references: [id])
|
||
externalCauseId String? @db.Uuid
|
||
externalCause ReferenceValue? @relation("BilanExternalCause", fields: [externalCauseId], references: [id])
|
||
actionTakenId String? @db.Uuid
|
||
actionTaken ReferenceValue? @relation("BilanActionTaken", fields: [actionTakenId], references: [id])
|
||
componentConcernedId String? @db.Uuid
|
||
componentConcerned ReferenceValue? @relation("BilanComponentConcerned", fields: [componentConcernedId], references: [id])
|
||
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
model TaskTemplate {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
label String @unique
|
||
componentTypeId String? @db.Uuid
|
||
componentType Category? @relation(fields: [componentTypeId], references: [id])
|
||
periodMonths Int // 1, 3, 6, 12…
|
||
isRegulatory Boolean @default(false) // essai parachute
|
||
isActive Boolean @default(true)
|
||
checklistItems ChecklistItem[]
|
||
}
|
||
|
||
model ChecklistItem {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
workOrderId String @db.Uuid
|
||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||
label String
|
||
state ChecklistState @default(PENDING)
|
||
templateId String? @db.Uuid
|
||
template TaskTemplate? @relation(fields: [templateId], references: [id])
|
||
doneById String? @db.Uuid
|
||
doneBy User? @relation(fields: [doneById], references: [id])
|
||
doneAt DateTime?
|
||
|
||
@@index([workOrderId])
|
||
}
|
||
|
||
model Meter {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
assetId String @db.Uuid
|
||
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||
kind MeterKind
|
||
readings MeterReading[]
|
||
|
||
@@unique([assetId, kind])
|
||
}
|
||
|
||
model MeterReading {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
meterId String @db.Uuid
|
||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||
value Int // strictement croissant (service)
|
||
readById String? @db.Uuid
|
||
readBy User? @relation(fields: [readById], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([meterId])
|
||
}
|