// 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[] 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[] @@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[] 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[] }