mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r2.1): socle backend exploitation — machine à états, bilan codé, demandes 1-1
- 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>
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkOrderType" AS ENUM ('CORRECTIVE', 'PREVENTIVE', 'WORKS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkOrderStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'ON_HOLD', 'DONE', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkOrderPriority" AS ENUM ('NONE', 'LOW', 'MEDIUM', 'HIGH', 'PERSON_TRAPPED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RequestStatus" AS ENUM ('RECEIVED', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ChecklistState" AS ENUM ('PENDING', 'DONE', 'NA');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BilanField" AS ENUM ('DOOR_STATE', 'CABIN_POSITION', 'ANOMALY', 'EXTERNAL_CAUSE', 'ACTION_TAKEN', 'COMPONENT_CONCERNED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MeterKind" AS ENUM ('RUNNING_HOURS', 'STARTS');
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Location_position_gix";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Location" DROP COLUMN "position";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkOrder" (
|
||||
"id" UUID NOT NULL,
|
||||
"reference" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" "WorkOrderType" NOT NULL,
|
||||
"status" "WorkOrderStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"priority" "WorkOrderPriority" NOT NULL DEFAULT 'NONE',
|
||||
"assetId" UUID NOT NULL,
|
||||
"dueDate" TIMESTAMP(3),
|
||||
"createdById" UUID,
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkOrder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkOrderEvent" (
|
||||
"id" UUID NOT NULL,
|
||||
"workOrderId" UUID NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"message" TEXT,
|
||||
"byId" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "WorkOrderEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Request" (
|
||||
"id" UUID NOT NULL,
|
||||
"reference" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"isPersonTrapped" BOOLEAN NOT NULL DEFAULT false,
|
||||
"status" "RequestStatus" NOT NULL DEFAULT 'RECEIVED',
|
||||
"rejectionReason" TEXT,
|
||||
"assetId" UUID NOT NULL,
|
||||
"requestedById" UUID,
|
||||
"requesterName" TEXT,
|
||||
"workOrderId" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Request_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ReferenceValue" (
|
||||
"id" UUID NOT NULL,
|
||||
"field" "BilanField" NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "ReferenceValue_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InterventionReport" (
|
||||
"id" UUID NOT NULL,
|
||||
"workOrderId" UUID NOT NULL,
|
||||
"note" TEXT,
|
||||
"doorStateId" UUID,
|
||||
"cabinPositionId" UUID,
|
||||
"anomalyId" UUID,
|
||||
"externalCauseId" UUID,
|
||||
"actionTakenId" UUID,
|
||||
"componentConcernedId" UUID,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "InterventionReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TaskTemplate" (
|
||||
"id" UUID NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"componentTypeId" UUID,
|
||||
"periodMonths" INTEGER NOT NULL,
|
||||
"isRegulatory" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "TaskTemplate_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ChecklistItem" (
|
||||
"id" UUID NOT NULL,
|
||||
"workOrderId" UUID NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"state" "ChecklistState" NOT NULL DEFAULT 'PENDING',
|
||||
"templateId" UUID,
|
||||
"doneById" UUID,
|
||||
"doneAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "ChecklistItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Meter" (
|
||||
"id" UUID NOT NULL,
|
||||
"assetId" UUID NOT NULL,
|
||||
"kind" "MeterKind" NOT NULL,
|
||||
|
||||
CONSTRAINT "Meter_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MeterReading" (
|
||||
"id" UUID NOT NULL,
|
||||
"meterId" UUID NOT NULL,
|
||||
"value" INTEGER NOT NULL,
|
||||
"readById" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MeterReading_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_WorkOrderAssignees" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "_WorkOrderAssignees_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WorkOrder_reference_key" ON "WorkOrder"("reference");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkOrder_assetId_idx" ON "WorkOrder"("assetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkOrder_status_idx" ON "WorkOrder"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkOrderEvent_workOrderId_idx" ON "WorkOrderEvent"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Request_reference_key" ON "Request"("reference");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Request_workOrderId_key" ON "Request"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Request_assetId_idx" ON "Request"("assetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ReferenceValue_field_label_key" ON "ReferenceValue"("field", "label");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InterventionReport_workOrderId_key" ON "InterventionReport"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TaskTemplate_label_key" ON "TaskTemplate"("label");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ChecklistItem_workOrderId_idx" ON "ChecklistItem"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Meter_assetId_kind_key" ON "Meter"("assetId", "kind");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "MeterReading_meterId_idx" ON "MeterReading"("meterId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_WorkOrderAssignees_B_index" ON "_WorkOrderAssignees"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkOrder" ADD CONSTRAINT "WorkOrder_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkOrder" ADD CONSTRAINT "WorkOrder_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkOrderEvent" ADD CONSTRAINT "WorkOrderEvent_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkOrderEvent" ADD CONSTRAINT "WorkOrderEvent_byId_fkey" FOREIGN KEY ("byId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Request" ADD CONSTRAINT "Request_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Request" ADD CONSTRAINT "Request_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Request" ADD CONSTRAINT "Request_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_doorStateId_fkey" FOREIGN KEY ("doorStateId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_cabinPositionId_fkey" FOREIGN KEY ("cabinPositionId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_anomalyId_fkey" FOREIGN KEY ("anomalyId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_externalCauseId_fkey" FOREIGN KEY ("externalCauseId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_actionTakenId_fkey" FOREIGN KEY ("actionTakenId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InterventionReport" ADD CONSTRAINT "InterventionReport_componentConcernedId_fkey" FOREIGN KEY ("componentConcernedId") REFERENCES "ReferenceValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TaskTemplate" ADD CONSTRAINT "TaskTemplate_componentTypeId_fkey" FOREIGN KEY ("componentTypeId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ChecklistItem" ADD CONSTRAINT "ChecklistItem_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ChecklistItem" ADD CONSTRAINT "ChecklistItem_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "TaskTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ChecklistItem" ADD CONSTRAINT "ChecklistItem_doneById_fkey" FOREIGN KEY ("doneById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Meter" ADD CONSTRAINT "Meter_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MeterReading" ADD CONSTRAINT "MeterReading_meterId_fkey" FOREIGN KEY ("meterId") REFERENCES "Meter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "MeterReading" ADD CONSTRAINT "MeterReading_readById_fkey" FOREIGN KEY ("readById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_WorkOrderAssignees" ADD CONSTRAINT "_WorkOrderAssignees_A_fkey" FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_WorkOrderAssignees" ADD CONSTRAINT "_WorkOrderAssignees_B_fkey" FOREIGN KEY ("B") REFERENCES "WorkOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -49,6 +49,13 @@ model User {
|
||||
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
|
||||
|
||||
@@ -75,6 +82,7 @@ model Category {
|
||||
isActive Boolean @default(true) // désactivable, jamais supprimée si utilisée
|
||||
assets Asset[]
|
||||
components AssetComponent[]
|
||||
taskTemplates TaskTemplate[]
|
||||
|
||||
@@unique([kind, name])
|
||||
}
|
||||
@@ -115,6 +123,9 @@ model Asset {
|
||||
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
|
||||
|
||||
@@ -141,3 +152,201 @@ model Team {
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ export async function seed(prisma: PrismaClient): Promise<void> {
|
||||
);
|
||||
await seedUsers(prisma, roleIds, passwordHash);
|
||||
await seedReferentiel(prisma);
|
||||
await seedExploitation(prisma);
|
||||
}
|
||||
|
||||
async function seedUsers(
|
||||
@@ -358,6 +359,227 @@ async function seedReferentiel(prisma: PrismaClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ————— R2 — Exploitation (référentiels du bilan, gabarits, données maquette) —————
|
||||
|
||||
const REFERENCE_VALUES: Record<string, string[]> = {
|
||||
DOOR_STATE: [
|
||||
'Fonctionnement normal',
|
||||
'Porte bloquée ouverte',
|
||||
'Porte bloquée fermée',
|
||||
'Fermeture incomplète',
|
||||
'Réouverture intempestive',
|
||||
],
|
||||
CABIN_POSITION: ['À niveau', 'Entre deux niveaux', 'Cuvette', 'Dernier niveau'],
|
||||
ANOMALY: [
|
||||
'Frottement mécanique',
|
||||
'Défaut électrique',
|
||||
'Usure normale',
|
||||
'Choc / vandalisme',
|
||||
'Aucune anomalie constatée',
|
||||
],
|
||||
EXTERNAL_CAUSE: ['Coupure électrique', 'Dégât des eaux', 'Mauvais usage', 'Aucune'],
|
||||
ACTION_TAKEN: [
|
||||
'Réglage',
|
||||
'Remplacement de pièce',
|
||||
'Nettoyage / graissage',
|
||||
'Remise en service simple',
|
||||
'Visite d’entretien',
|
||||
'Attente de pièce',
|
||||
],
|
||||
COMPONENT_CONCERNED: [
|
||||
'Portes',
|
||||
'Guides',
|
||||
'Treuil / machinerie',
|
||||
'Armoire de commande',
|
||||
'Boutons / signalisation',
|
||||
'Parachute',
|
||||
'Cabine',
|
||||
],
|
||||
};
|
||||
|
||||
/** Gabarits du préventif (maquette R2) — période calendaire en mois. */
|
||||
const TASK_TEMPLATES: {
|
||||
label: string;
|
||||
periodMonths: number;
|
||||
componentType?: string;
|
||||
isRegulatory?: boolean;
|
||||
}[] = [
|
||||
{ label: 'Contrôle fermeture / verrouillage des portes', periodMonths: 1, componentType: 'Portes cabine / palières' },
|
||||
{ label: 'Nettoyage cuvette et toit de cabine', periodMonths: 1 },
|
||||
{ label: 'Contrôle boutons cabine & paliers', periodMonths: 1, componentType: 'Boutons & signalisation' },
|
||||
{ label: 'Vérification éclairage de secours', periodMonths: 1 },
|
||||
{ label: 'Contrôle niveau d’huile réducteur', periodMonths: 3, componentType: 'Treuil / machinerie' },
|
||||
{ label: 'Vérification jeu des coulisseaux', periodMonths: 6 },
|
||||
{ label: 'Contrôle câbles de traction (usure, tension)', periodMonths: 6, componentType: 'Treuil / machinerie' },
|
||||
{ label: 'Essai du parachute', periodMonths: 12, componentType: 'Parachute', isRegulatory: true },
|
||||
];
|
||||
|
||||
async function seedExploitation(prisma: PrismaClient): Promise<void> {
|
||||
const refIds = new Map<string, string>(); // « FIELD/label » → id
|
||||
for (const [field, labels] of Object.entries(REFERENCE_VALUES)) {
|
||||
for (const label of labels) {
|
||||
const value = await prisma.referenceValue.upsert({
|
||||
where: { field_label: { field: field as never, label } },
|
||||
update: {},
|
||||
create: { field: field as never, label },
|
||||
});
|
||||
refIds.set(`${field}/${label}`, value.id);
|
||||
}
|
||||
}
|
||||
|
||||
const componentTypes = new Map(
|
||||
(await prisma.category.findMany({ where: { kind: 'COMPONENT_TYPE' } })).map((c) => [
|
||||
c.name,
|
||||
c.id,
|
||||
]),
|
||||
);
|
||||
for (const t of TASK_TEMPLATES) {
|
||||
await prisma.taskTemplate.upsert({
|
||||
where: { label: t.label },
|
||||
update: {},
|
||||
create: {
|
||||
label: t.label,
|
||||
periodMonths: t.periodMonths,
|
||||
isRegulatory: t.isRegulatory ?? false,
|
||||
componentTypeId: t.componentType ? componentTypes.get(t.componentType) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Données de démonstration (rejouent la maquette : OT en cours, urgence,
|
||||
// grille du mois, demandes à approuver). Idempotent par référence.
|
||||
const parReference = async (ref: string) =>
|
||||
(await prisma.asset.findUniqueOrThrow({ where: { reference: ref } })).id;
|
||||
const parEmail = async (email: string) =>
|
||||
(await prisma.user.findUniqueOrThrow({ where: { email } })).id;
|
||||
|
||||
const a1 = await parReference('A1');
|
||||
const b2 = await parReference('B2');
|
||||
const c1 = await parReference('C1');
|
||||
const ahmed = await parEmail('technicien@demo.siop.ma');
|
||||
const salma = await parEmail('dispatcher@demo.siop.ma');
|
||||
const karim = await parEmail('demandeur@demo.siop.ma');
|
||||
const annee = new Date().getFullYear();
|
||||
|
||||
const grilleLabels = TASK_TEMPLATES.filter((t) => t.periodMonths === 1).map(
|
||||
(t) => t.label,
|
||||
);
|
||||
|
||||
const OTS: {
|
||||
ref: string; title: string; type: 'CORRECTIVE' | 'PREVENTIVE' | 'WORKS';
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'ON_HOLD' | 'DONE' | 'CANCELLED';
|
||||
priority: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH' | 'PERSON_TRAPPED';
|
||||
assetId: string; assignees?: string[]; dueJours?: number;
|
||||
checklist?: string[]; bilan?: Record<string, string>;
|
||||
}[] = [
|
||||
{
|
||||
ref: `OT-${annee}-0342`, title: 'Personne bloquée en cabine', type: 'CORRECTIVE',
|
||||
status: 'OPEN', priority: 'PERSON_TRAPPED', assetId: b2,
|
||||
},
|
||||
{
|
||||
ref: `OT-${annee}-0341`, title: 'Bruit anormal en gaine', type: 'CORRECTIVE',
|
||||
status: 'IN_PROGRESS', priority: 'HIGH', assetId: a1, assignees: [ahmed], dueJours: 2,
|
||||
},
|
||||
{
|
||||
ref: `OT-${annee}-0338`, title: 'Grille du mois — juillet', type: 'PREVENTIVE',
|
||||
status: 'IN_PROGRESS', priority: 'LOW', assetId: a1, assignees: [ahmed],
|
||||
dueJours: 15, checklist: grilleLabels,
|
||||
},
|
||||
{
|
||||
ref: `OT-${annee}-0332`, title: 'Réglage nivellement cabine', type: 'CORRECTIVE',
|
||||
status: 'DONE', priority: 'LOW', assetId: a1,
|
||||
bilan: {
|
||||
doorStateId: refIds.get('DOOR_STATE/Fonctionnement normal')!,
|
||||
actionTakenId: refIds.get('ACTION_TAKEN/Réglage')!,
|
||||
componentConcernedId: refIds.get('COMPONENT_CONCERNED/Guides')!,
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const ot of OTS) {
|
||||
const existant = await prisma.workOrder.findUnique({ where: { reference: ot.ref } });
|
||||
if (existant) continue;
|
||||
await prisma.workOrder.create({
|
||||
data: {
|
||||
reference: ot.ref,
|
||||
title: ot.title,
|
||||
type: ot.type,
|
||||
status: ot.status,
|
||||
priority: ot.priority,
|
||||
assetId: ot.assetId,
|
||||
createdById: salma,
|
||||
dueDate: ot.dueJours
|
||||
? new Date(Date.now() + ot.dueJours * 24 * 3600 * 1000)
|
||||
: undefined,
|
||||
startedAt: ot.status === 'IN_PROGRESS' || ot.status === 'DONE' ? new Date() : undefined,
|
||||
completedAt: ot.status === 'DONE' ? new Date() : undefined,
|
||||
assignees: ot.assignees ? { connect: ot.assignees.map((id) => ({ id })) } : undefined,
|
||||
events: { create: { kind: 'CREATED', message: 'OT créé (seed)', byId: salma } },
|
||||
checklist: ot.checklist
|
||||
? { create: ot.checklist.map((label) => ({ label })) }
|
||||
: undefined,
|
||||
report: ot.bilan ? { create: ot.bilan } : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const DEMANDES: {
|
||||
ref: string; description: string; assetId: string; isPersonTrapped?: boolean;
|
||||
status: 'RECEIVED' | 'APPROVED' | 'REJECTED';
|
||||
rejectionReason?: string; otRef?: string;
|
||||
}[] = [
|
||||
{
|
||||
ref: `DEM-${annee}-0111`, assetId: a1, status: 'RECEIVED',
|
||||
description: 'La porte ne se ferme plus au 3ᵉ étage, il faut la retenir à la main.',
|
||||
},
|
||||
{ ref: `DEM-${annee}-0110`, assetId: c1, status: 'RECEIVED', description: 'Voyant étage éteint.' },
|
||||
{
|
||||
ref: `DEM-${annee}-0107`, assetId: a1, status: 'APPROVED',
|
||||
description: 'Bruit anormal en gaine.', otRef: `OT-${annee}-0341`,
|
||||
},
|
||||
{
|
||||
ref: `DEM-${annee}-0104`, assetId: b2, status: 'REJECTED',
|
||||
description: 'Odeur de brûlé.', rejectionReason: 'Fausse alerte confirmée sur place par le gardien.',
|
||||
},
|
||||
];
|
||||
for (const dem of DEMANDES) {
|
||||
const existant = await prisma.request.findUnique({ where: { reference: dem.ref } });
|
||||
if (existant) continue;
|
||||
const workOrderId = dem.otRef
|
||||
? (await prisma.workOrder.findUnique({ where: { reference: dem.otRef } }))?.id
|
||||
: undefined;
|
||||
await prisma.request.create({
|
||||
data: {
|
||||
reference: dem.ref,
|
||||
description: dem.description,
|
||||
isPersonTrapped: dem.isPersonTrapped ?? false,
|
||||
status: dem.status,
|
||||
rejectionReason: dem.rejectionReason,
|
||||
assetId: dem.assetId,
|
||||
requestedById: karim,
|
||||
workOrderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Compteurs de la maquette (A1)
|
||||
for (const [kind, valeurs] of [
|
||||
['RUNNING_HOURS', [12246, 12322, 12411]],
|
||||
['STARTS', [1815400, 1831970]],
|
||||
] as const) {
|
||||
const meter = await prisma.meter.upsert({
|
||||
where: { assetId_kind: { assetId: a1, kind } },
|
||||
update: {},
|
||||
create: { assetId: a1, kind },
|
||||
include: { _count: { select: { readings: true } } },
|
||||
});
|
||||
if (meter._count.readings === 0) {
|
||||
await prisma.meterReading.createMany({
|
||||
data: valeurs.map((value) => ({ meterId: meter.id, value, readById: ahmed })),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* c8 ignore start — wrapper CLI */
|
||||
if (require.main === module) {
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
Reference in New Issue
Block a user