// 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]) }