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:
pr-daaif
2026-07-16 13:56:07 +01:00
parent 54d926e9f4
commit f7702e4252
23 changed files with 5568 additions and 4 deletions

View File

@@ -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;

View File

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

View File

@@ -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 dentretien',
'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 dhuile 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();