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