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>
201 lines
9.5 KiB
Markdown
201 lines
9.5 KiB
Markdown
# Modèle de données — SIOP V2
|
||
|
||
> **Rôle de ce document (playbook)** : le modèle est introduit **par release** (on ne
|
||
> modélise pas ce qu'on ne construit pas). Chaque section explique les invariants et OÙ
|
||
> ils vivent (base / service). La v1 sert de référence éprouvée pour les entités à venir.
|
||
|
||
## R0 — Identité & permissions
|
||
|
||
```prisma
|
||
model Role {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique // Administrateur, Dispatcher, Technicien,
|
||
users User[] // Technicien limité, Gestionnaire,
|
||
permissions Permission[] // Demandeur, Vue seule
|
||
}
|
||
|
||
model Permission { // matrice rôles × objets × droits — EN BASE, jamais dans le JWT
|
||
id String @id @default(uuid()) @db.Uuid
|
||
roleId String @db.Uuid
|
||
role Role @relation(...)
|
||
objectCategory String // enum applicatif : WORK_ORDERS, ASSETS, …
|
||
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)
|
||
roleId String @db.Uuid
|
||
role Role @relation(...)
|
||
isActive Boolean @default(true)
|
||
isDemo Boolean @default(false) // seul un compte isDemo est empruntable (ADR-002)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
```
|
||
|
||
**Invariants R0** :
|
||
|
||
| Invariant | Où il vit |
|
||
| --- | --- |
|
||
| Un rôle par utilisateur ; la matrice décide de tout accès | `PermissionsGuard` (relit la base, cache 60 s) |
|
||
| Le JWT ne porte jamais de droits (seulement l'identité + roleId) | conception auth |
|
||
| `demo-login` refuse tout compte `isDemo=false` | service auth (+ test e2e) |
|
||
| Matrice complète : chaque rôle a une ligne par catégorie d'objet | seed idempotent (+ test) |
|
||
|
||
## R1 — Référentiel
|
||
|
||
```prisma
|
||
enum CategoryKind { EQUIPMENT COMPONENT_TYPE } // référentiels administrables
|
||
enum AssetStatus { IN_SERVICE OUT_OF_SERVICE UNDER_MAINTENANCE }
|
||
|
||
model Category { // renommable/désactivable, JAMAIS supprimée si utilisée
|
||
id String @id @default(uuid()) @db.Uuid
|
||
kind CategoryKind
|
||
name String
|
||
isActive Boolean @default(true)
|
||
@@unique([kind, name])
|
||
}
|
||
|
||
model Location { // site (parentId null) → zone (1 niveau max, vérifié service)
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String
|
||
parentId String? @db.Uuid // auto-relation « LocationTree »
|
||
address String? city String?
|
||
guardianName String? guardianPhone String?
|
||
latitude Float? longitude Float? // saisies par la carte
|
||
// + colonne PostGIS générée (migration SQL) :
|
||
// position geography(Point,4326) GENERATED ALWAYS AS (ST_Point(longitude,latitude)::geography) STORED
|
||
}
|
||
|
||
model Asset { // l'appareil (ascenseur, monte-charge…)
|
||
id String @id @default(uuid()) @db.Uuid
|
||
reference String @unique // « A1 », « B2 » — imprimée sur le 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(kind=EQUIPMENT)
|
||
locationId String @db.Uuid // rattachement obligatoire
|
||
components AssetComponent[]
|
||
}
|
||
|
||
model AssetComponent { // organe — PAS de colonne emplacement : la règle
|
||
id String @id @default(uuid()) @db.Uuid // « un organe n'a pas d'emplacement
|
||
assetId String @db.Uuid // propre » est garantie PAR
|
||
typeId String @db.Uuid // CONSTRUCTION (table dédiée),
|
||
designation String? // plus besoin du CHECK v1
|
||
}
|
||
|
||
model Team { // équipes par zone — l'assignation d'OT (R2) s'appuiera dessus
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique
|
||
description String?
|
||
members User[] // m2m implicite
|
||
}
|
||
|
||
// User (R0) reçoit : phone?, teams Team[], et l'invitation (ADR maquettes R1) :
|
||
// activationToken String? @unique + activationExpiresAt DateTime?
|
||
// statut dérivé : invité = passwordHash null && token présent ; actif = hash présent
|
||
```
|
||
|
||
**Invariants R1** :
|
||
|
||
| Invariant | Où il vit |
|
||
| --- | --- |
|
||
| Hiérarchie d'emplacements limitée à 2 niveaux (site → zone) | service locations (+ test) |
|
||
| Un organe n'a pas d'emplacement propre | par construction (AssetComponent sans locationId) |
|
||
| `Category(kind)` cohérente avec l'usage (EQUIPMENT sur Asset, COMPONENT_TYPE sur organe) | services (+ test) |
|
||
| Catégorie utilisée : jamais supprimée (désactivation seulement) | service categories (+ test) |
|
||
| Lien d'activation : 7 jours, usage unique, aucun compte actif avant | service users (+ test e2e) |
|
||
| Position : lat/lng saisis, colonne PostGIS **générée** pour les requêtes spatiales futures | migration SQL |
|
||
|
||
## R2 — Exploitation
|
||
|
||
```prisma
|
||
enum WorkOrderType { CORRECTIVE PREVENTIVE WORKS } // Dépannage / Maintenance / Travaux
|
||
enum WorkOrderStatus { OPEN IN_PROGRESS ON_HOLD DONE CANCELLED }
|
||
enum WorkOrderPriority { NONE LOW MEDIUM HIGH PERSON_TRAPPED } // personne bloquée = priorité, pas statut
|
||
enum RequestStatus { RECEIVED APPROVED REJECTED } // « Résolue » = dérivé (OT lié DONE)
|
||
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 { // machine à états STRICTE (service, table des transitions)
|
||
id String @id … reference String @unique // OT-2026-0341 (séquence + retry)
|
||
title String description String?
|
||
type WorkOrderType status WorkOrderStatus @default(OPEN)
|
||
priority WorkOrderPriority @default(NONE)
|
||
assetId → Asset dueDate DateTime?
|
||
assignees User[] ("WorkOrderAssignees") createdById → User?
|
||
startedAt? completedAt? cancelledAt?
|
||
events WorkOrderEvent[] checklist ChecklistItem[]
|
||
report InterventionReport? request Request?
|
||
}
|
||
|
||
model WorkOrderEvent { // activité chronologique : commentaires + traces de transition
|
||
id, workOrderId (cascade), kind String, message String?, byId → User?, createdAt
|
||
}
|
||
|
||
model Request { // demande — converge vers l'OT (lien 1-1, jamais de doublon)
|
||
id, reference @unique // DEM-2026-0112
|
||
description, isPersonTrapped Boolean @default(false)
|
||
status RequestStatus @default(RECEIVED) rejectionReason String? // motif REQUIS au rejet
|
||
assetId → Asset requestedById → User? requesterName String? // portail public R2.4
|
||
workOrderId String? @unique → WorkOrder
|
||
}
|
||
|
||
model ReferenceValue { // référentiels ADMINISTRABLES du bilan codé (un par champ)
|
||
id, field BilanField, label String, isActive Boolean @default(true)
|
||
@@unique([field, label])
|
||
}
|
||
|
||
model InterventionReport { // bilan codé 6 champs — 1-1 avec l'OT
|
||
id, workOrderId @unique (cascade), note String?
|
||
doorStateId? cabinPositionId? anomalyId? externalCauseId? actionTakenId? componentConcernedId?
|
||
// requis pour clôturer : DOOR_STATE, ACTION_TAKEN, COMPONENT_CONCERNED (maquette)
|
||
}
|
||
|
||
model TaskTemplate { // gabarit du préventif (période calendaire en mois)
|
||
id, label, componentTypeId? → Category, periodMonths Int, isRegulatory Boolean, isActive
|
||
}
|
||
model ChecklistItem { // la grille du mois d'un OT préventif
|
||
id, workOrderId (cascade), label, state ChecklistState @default(PENDING)
|
||
templateId? → TaskTemplate doneById? → User doneAt?
|
||
}
|
||
|
||
model Meter { id, assetId → Asset, kind MeterKind, @@unique([assetId, kind]) }
|
||
model MeterReading { id, meterId (cascade), value Int, readById? → User, createdAt }
|
||
```
|
||
|
||
**Invariants R2** :
|
||
|
||
| Invariant | Où il vit |
|
||
| --- | --- |
|
||
| Transitions : OPEN→(IN_PROGRESS·CANCELLED) ; IN_PROGRESS→(ON_HOLD·DONE·CANCELLED) ; ON_HOLD→(IN_PROGRESS·CANCELLED) ; DONE/CANCELLED terminaux | service work-orders (+ tests) |
|
||
| **Garde de clôture** : bilan (3 champs requis) + checklist sans PENDING | service (transition → DONE) |
|
||
| Demande approuvée = **un seul** OT (1-1), rejet ⇒ motif obligatoire | contrainte @unique + service |
|
||
| « Voir autre » : sans `canViewOther`, un rôle ne voit que SES objets (assigné/créateur/demandeur) | services (scoping des listes + accès) |
|
||
| ReferenceValue/TaskTemplate utilisés : désactivables, jamais supprimés | services (même règle que Category) |
|
||
| Relevé de compteur strictement croissant | service meters |
|
||
| Génération mensuelle idempotente ; appareil sans historique ⇒ premier contrôle (toutes tâches) | service préventif (R2.2, + tests) |
|
||
|
||
## À venir (référence v1 éprouvée, sera réintroduit release par release)
|
||
|
||
- **R3** : `Part`/`StockMovement` (stock **dérivé des mouvements**), `PurchaseOrder`,
|
||
`Partner`, `LaborTime` (taux figé), `Document`.
|
||
- **R4** : `WorkOrder.version` (verrou optimiste de la synchro mobile).
|
||
- **R5** : tables d'embeddings pgvector (côté service IA).
|
||
|
||
## Journal des migrations
|
||
|
||
| # | Migration | Contenu |
|
||
| --- | --- | --- |
|
||
| 1 | `r0_identity` | Role, Permission, User (+ index & uniques ci-dessus) |
|