// 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[] // R3 — gestion : taux horaire COURANT (le taux d'une saisie est figé dans LaborTime) hourlyRate Decimal? @db.Decimal(8, 2) laborTimes LaborTime[] stockMovements StockMovement[] purchaseOrders PurchaseOrder[] documents Document[] 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 // Client/syndic gérant le site (R3 — colonne « Rattachements » des Tiers) partnerId String? @db.Uuid partner Partner? @relation(fields: [partnerId], references: [id]) assets Asset[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([parentId]) @@index([partnerId]) } 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 underContract Boolean @default(true) // contrat préventif (grille mensuelle) 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[] documents Document[] 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[] ragChunks RagChunk[] checklist ChecklistItem[] report InterventionReport? request Request? stockMovements StockMovement[] laborTimes LaborTime[] documents Document[] // Grille du mois : « AAAA-MM » — l'unicité [assetId, periodKey] EST // l'idempotence de la génération (les OT correctifs restent à null). periodKey String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([assetId, periodKey]) @@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) // Suivi SANS COMPTE (portail) : le téléphone du gardien garde ce jeton, // seul moyen de lire l'avancement — jamais listé, jamais devinable. publicToken String? @unique 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]) } // ————— R3 — Gestion (docs/03-architecture/modele-donnees.md §R3) ————— enum PartnerKind { SUPPLIER CLIENT // syndic / propriétaire } enum PurchaseOrderStatus { DRAFT SENT RECEIVED CANCELLED } enum StockMovementKind { RECEIPT // réception de BC (+, PU figé) ENTRY // entrée manuelle (+) CONSUMPTION // consommation d'OT (−, PU figé) ADJUSTMENT // inventaire (±, motif REQUIS) } enum DocumentKind { NOTICE CERTIFICATE PHOTO OTHER } model Partner { id String @id @default(uuid()) @db.Uuid name String @unique kind PartnerKind contactName String? phone String? email String? city String? isActive Boolean @default(true) parts Part[] purchaseOrders PurchaseOrder[] sites Location[] // sites gérés (kind CLIENT) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Part { id String @id @default(uuid()) @db.Uuid reference String @unique // P-0113 designation String threshold Int @default(0) // seuil d'alerte // Dernier prix d'achat — figé sur chaque mouvement au moment T. lastUnitPrice Decimal? @db.Decimal(10, 2) compatible String? supplierId String? @db.Uuid supplier Partner? @relation(fields: [supplierId], references: [id]) isActive Boolean @default(true) movements StockMovement[] orderLines PurchaseOrderLine[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Le stock EST la somme des mouvements — aucune colonne de quantité. model StockMovement { id String @id @default(uuid()) @db.Uuid partId String @db.Uuid part Part @relation(fields: [partId], references: [id], onDelete: Cascade) kind StockMovementKind quantity Int // signé : + entrée, − sortie unitPrice Decimal? @db.Decimal(10, 2) // figé (réception, consommation) reason String? // ajustement : motif requis (service) workOrderId String? @db.Uuid workOrder WorkOrder? @relation(fields: [workOrderId], references: [id]) purchaseOrderId String? @db.Uuid purchaseOrder PurchaseOrder? @relation(fields: [purchaseOrderId], references: [id]) byId String? @db.Uuid by User? @relation(fields: [byId], references: [id]) createdAt DateTime @default(now()) @@index([partId]) @@index([workOrderId]) } model PurchaseOrder { id String @id @default(uuid()) @db.Uuid reference String @unique // BC-2026-0024 status PurchaseOrderStatus @default(DRAFT) supplierId String @db.Uuid supplier Partner @relation(fields: [supplierId], references: [id]) lines PurchaseOrderLine[] movements StockMovement[] sentAt DateTime? receivedAt DateTime? cancelledAt DateTime? createdById String? @db.Uuid createdBy User? @relation(fields: [createdById], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model PurchaseOrderLine { id String @id @default(uuid()) @db.Uuid purchaseOrderId String @db.Uuid purchaseOrder PurchaseOrder @relation(fields: [purchaseOrderId], references: [id], onDelete: Cascade) partId String @db.Uuid part Part @relation(fields: [partId], references: [id]) quantity Int unitPrice Decimal @db.Decimal(10, 2) @@index([purchaseOrderId]) } // Main-d'œuvre : le taux est FIGÉ à la saisie (le coût d'un OT ne bouge plus). model LaborTime { id String @id @default(uuid()) @db.Uuid workOrderId String @db.Uuid workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade) userId String @db.Uuid user User @relation(fields: [userId], references: [id]) minutes Int hourlyRate Decimal @db.Decimal(8, 2) note String? createdAt DateTime @default(now()) @@index([workOrderId]) } model Document { id String @id @default(uuid()) @db.Uuid kind DocumentKind fileName String storageKey String @unique // clé MinIO (FileStorage) size Int contentType String assetId String? @db.Uuid asset Asset? @relation(fields: [assetId], references: [id]) workOrderId String? @db.Uuid workOrder WorkOrder? @relation(fields: [workOrderId], references: [id]) uploadedById String? @db.Uuid uploadedBy User? @relation(fields: [uploadedById], references: [id]) createdAt DateTime @default(now()) // R5 (D3) : le corpus est visible et réversible, document par document. inCorpus Boolean @default(true) indexedAt DateTime? // null = jamais indexé chunkCount Int @default(0) chunks RagChunk[] @@index([assetId]) @@index([workOrderId]) } /// R5 — extraits indexés du corpus (ADR-004) : bibliothèque + bilans codés, /// ANONYMISÉS À L'INGESTION (D4). Écrit par apps/ai, schéma possédé par Prisma. model RagChunk { id String @id @default(uuid()) @db.Uuid sourceType RagSourceType documentId String? @db.Uuid document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) workOrderId String? @db.Uuid workOrder WorkOrder? @relation(fields: [workOrderId], references: [id], onDelete: Cascade) /// Repère humain de la source : « p. 42 », « bilan du 17/07/2026 »… locator String content String embedding Unsupported("vector(768)") createdAt DateTime @default(now()) @@index([documentId]) @@index([workOrderId]) } enum RagSourceType { DOCUMENT WORK_ORDER }