/** * Seed R0 — idempotent (upserts) : 7 rôles, matrice COMPLÈTE (une ligne par * rôle × catégorie, invariant R0) et 7 comptes de démonstration (ADR-002). * La matrice ci-dessous est le point de DÉPART pédagogique : elle vit en base * et sera administrable (R1) — la modifier ici ne change pas une base déjà * seedée (les lignes existantes ne sont pas écrasées, voir plus bas). * Usage : pnpm seed (ou prisma db seed) */ import 'dotenv/config'; import { PrismaClient } from '@prisma/client'; import { OBJECT_CATEGORIES, ROLE_NAMES, type ObjectCategory, type RoleName, } from '@siop/shared'; import * as argon2 from 'argon2'; type Grant = Partial<{ view: boolean; viewOther: boolean; create: boolean; edit: boolean; delete: boolean; }>; const FULL: Grant = { view: true, viewOther: true, create: true, edit: true, delete: true }; const READ: Grant = { view: true, viewOther: true }; /** Matrice de départ (rôle → catégorie → droits ; absent = tout à false). */ const MATRIX: Record>> = { Administrateur: Object.fromEntries( OBJECT_CATEGORIES.map((c) => [c, FULL]), ) as Record, Gestionnaire: { WORK_ORDERS: FULL, REQUESTS: FULL, ASSETS: FULL, LOCATIONS: FULL, METERS: FULL, PARTS: FULL, PURCHASE_ORDERS: FULL, PEOPLE_TEAMS: { view: true, viewOther: true, create: true, edit: true }, ANALYTICS: READ, SETTINGS: { view: true }, }, Dispatcher: { WORK_ORDERS: { view: true, viewOther: true, create: true, edit: true }, REQUESTS: { view: true, viewOther: true, create: true, edit: true }, ASSETS: READ, LOCATIONS: READ, METERS: READ, PARTS: { view: true }, PEOPLE_TEAMS: READ, ANALYTICS: READ, }, Technicien: { WORK_ORDERS: { view: true, edit: true }, // ses OT uniquement (viewOther=false) REQUESTS: { view: true }, ASSETS: READ, LOCATIONS: READ, METERS: { view: true, viewOther: true, create: true }, // relevés PARTS: { view: true }, }, 'Technicien limité': { WORK_ORDERS: { view: true, edit: true }, // ses OT, sans consultation du parc ASSETS: { view: true }, }, Demandeur: { REQUESTS: { view: true, create: true }, // ses demandes uniquement }, 'Vue seule': { WORK_ORDERS: READ, REQUESTS: READ, ASSETS: READ, LOCATIONS: READ, METERS: READ, PARTS: READ, PURCHASE_ORDERS: READ, PEOPLE_TEAMS: READ, ANALYTICS: READ, }, }; /** 7 comptes démo — un par rôle. Les noms sont CEUX de la maquette validée * (maquette-web.html) : la revue pixel compare écrans et maquettes. */ const DEMO_USERS: { email: string; displayName: string; role: RoleName }[] = [ { email: 'admin@demo.siop.ma', displayName: 'Yasmine Alaoui', role: 'Administrateur' }, { email: 'dispatcher@demo.siop.ma', displayName: 'Salma Idrissi', role: 'Dispatcher' }, { email: 'technicien@demo.siop.ma', displayName: 'Ahmed Benali', role: 'Technicien' }, { email: 'technicien-limite@demo.siop.ma', displayName: 'Youssef Tazi', role: 'Technicien limité' }, { email: 'gestionnaire@demo.siop.ma', displayName: 'Nadia Berrada', role: 'Gestionnaire' }, { email: 'demandeur@demo.siop.ma', displayName: 'Karim Doukkali', role: 'Demandeur' }, { email: 'vue-seule@demo.siop.ma', displayName: 'Rachid Bennis', role: 'Vue seule' }, ]; export async function seed(prisma: PrismaClient): Promise { const roleIds = new Map(); for (const name of ROLE_NAMES) { const role = await prisma.role.upsert({ where: { name }, update: {}, create: { name }, }); roleIds.set(name, role.id); } // Matrice complète : une ligne par rôle × catégorie. Les lignes existantes // ne sont PAS écrasées (la base est la source de vérité, pas ce fichier). for (const name of ROLE_NAMES) { const roleId = roleIds.get(name)!; for (const category of OBJECT_CATEGORIES) { const g = MATRIX[name][category] ?? {}; await prisma.permission.upsert({ where: { roleId_objectCategory: { roleId, objectCategory: category } }, update: {}, create: { roleId, objectCategory: category, canView: g.view ?? false, canViewOther: g.viewOther ?? false, canCreate: g.create ?? false, canEdit: g.edit ?? false, canDelete: g.delete ?? false, }, }); } } // Mot de passe commun des comptes démo (la connexion classique reste testable). const passwordHash = await argon2.hash( process.env.SEED_DEMO_PASSWORD ?? 'Demo!2026', ); await seedUsers(prisma, roleIds, passwordHash); await seedReferentiel(prisma); await seedExploitation(prisma); await seedGestion(prisma); } async function seedUsers( prisma: PrismaClient, roleIds: Map, passwordHash: string, ): Promise { for (const u of DEMO_USERS) { // Les comptes démo appartiennent au seed : nom et rôle sont réalignés // à chaque exécution (jamais le mot de passe d'un compte existant). await prisma.user.upsert({ where: { email: u.email }, update: { displayName: u.displayName, roleId: roleIds.get(u.role)! }, create: { email: u.email, displayName: u.displayName, passwordHash, roleId: roleIds.get(u.role)!, isDemo: true, }, }); } } // ————— R1 — Référentiel (données des maquettes validées) ————— const EQUIPMENT_CATEGORIES = [ 'Ascenseur électrique', 'Ascenseur hydraulique', 'Monte-charge', 'EPMR (plateforme PMR)', ]; const COMPONENT_TYPES = [ 'Portes cabine / palières', 'Treuil / machinerie', 'Parachute', 'Armoire de commande', 'Boutons & signalisation', ]; /** site → zones ; positions réelles Casablanca/Mohammedia. */ const SITES: { name: string; address: string; city: string; guardianName?: string; guardianPhone?: string; latitude: number; longitude: number; zones: string[]; }[] = [ { name: 'Tour Atlas', address: 'Bd de la Corniche, Aïn Diab', city: 'Casablanca', guardianName: 'Karim Doukkali', guardianPhone: '06 61 23 45 67', latitude: 33.6062, longitude: -7.6706, zones: ['Hall principal', 'Tour bureaux (étages 1-24)', 'Parking sous-sol', 'Résidence (aile est)'], }, { name: 'Résidence Al Manar', address: 'Bd Hassan II', city: 'Mohammedia', guardianName: 'Hassan Alami', latitude: 33.6866, longitude: -7.383, zones: ['Hall principal'], }, { name: 'Anfa Place', address: 'Bd de l’Océan Atlantique', city: 'Casablanca', latitude: 33.5883, longitude: -7.6822, zones: ['Galerie commerciale'], }, { name: 'Clinique Yasmine', address: 'Rue Ibn Rochd', city: 'Casablanca', guardianName: 'Rachid Mansouri', latitude: 33.5731, longitude: -7.6316, zones: ['Bloc A'], }, { name: 'Marina Center', address: 'Av. des FAR', city: 'Mohammedia', latitude: 33.7, longitude: -7.39, zones: ['Hall B'], }, ]; const ASSETS: { reference: string; brand: string; model: string; serialNumber?: string; commissionedAt?: string; loadKg?: number; floors?: number; category: string; site: string; zone: string; status?: 'IN_SERVICE' | 'OUT_OF_SERVICE' | 'UNDER_MAINTENANCE'; components?: { type: string; designation?: string }[]; }[] = [ { reference: 'A1', brand: 'Otis', model: 'Gen2 Premier', serialNumber: 'OT-2020-4521', commissionedAt: '2020-03-15', loadKg: 630, floors: 8, category: 'Ascenseur électrique', site: 'Résidence Al Manar', zone: 'Hall principal', components: [ { type: 'Portes cabine / palières', designation: 'Fermator 40/10' }, { type: 'Treuil / machinerie', designation: 'Gen2 gearless' }, { type: 'Parachute' }, { type: 'Armoire de commande', designation: 'MCS 220' }, ], }, { reference: 'A2', brand: 'Otis', model: 'Gen2', loadKg: 630, floors: 12, category: 'Ascenseur électrique', site: 'Tour Atlas', zone: 'Hall principal', }, { reference: 'B1', brand: 'Schindler', model: '3300', loadKg: 1000, floors: 24, category: 'Ascenseur électrique', site: 'Tour Atlas', zone: 'Tour bureaux (étages 1-24)', }, { reference: 'B2', brand: 'Schindler', model: '3300', loadKg: 1000, floors: 24, category: 'Ascenseur électrique', site: 'Tour Atlas', zone: 'Tour bureaux (étages 1-24)', status: 'OUT_OF_SERVICE', components: [ { type: 'Portes cabine / palières', designation: 'Sematic' }, { type: 'Armoire de commande' }, ], }, { reference: 'M1', brand: 'Kone', model: 'TranSys', loadKg: 2000, floors: 3, category: 'Monte-charge', site: 'Tour Atlas', zone: 'Parking sous-sol', status: 'UNDER_MAINTENANCE', }, { reference: 'C1', brand: 'Kone', model: 'MonoSpace 500', loadKg: 800, floors: 5, category: 'Ascenseur électrique', site: 'Anfa Place', zone: 'Galerie commerciale', }, { reference: 'D1', brand: 'ThyssenKrupp', model: 'Evolution', loadKg: 1600, floors: 6, category: 'Ascenseur électrique', site: 'Clinique Yasmine', zone: 'Bloc A', }, { reference: 'E2', brand: 'Otis', model: 'HydroFit', loadKg: 630, floors: 4, category: 'Ascenseur hydraulique', site: 'Marina Center', zone: 'Hall B', }, ]; const TEAMS: { name: string; description: string; memberEmails: string[] }[] = [ { name: 'Casablanca Centre', description: 'Tour Atlas · Anfa Place · Clinique Yasmine', memberEmails: ['technicien@demo.siop.ma', 'technicien-limite@demo.siop.ma'], }, { name: 'Mohammedia', description: 'Résidence Al Manar · Marina Center', memberEmails: [], }, ]; async function seedReferentiel(prisma: PrismaClient): Promise { const categoryIds = new Map(); for (const [kind, names] of [ ['EQUIPMENT', EQUIPMENT_CATEGORIES], ['COMPONENT_TYPE', COMPONENT_TYPES], ] as const) { for (const name of names) { const category = await prisma.category.upsert({ where: { kind_name: { kind, name } }, update: {}, create: { kind, name }, }); categoryIds.set(name, category.id); } } // Pas d'unicité en base sur (parent, nom) : idempotence par findFirst. const zoneIds = new Map(); // « site / zone » → id for (const site of SITES) { const { zones, ...data } = site; let root = await prisma.location.findFirst({ where: { name: site.name, parentId: null }, }); root ??= await prisma.location.create({ data }); for (const zoneName of zones) { let zone = await prisma.location.findFirst({ where: { name: zoneName, parentId: root.id }, }); zone ??= await prisma.location.create({ data: { name: zoneName, parentId: root.id, city: site.city }, }); zoneIds.set(`${site.name} / ${zoneName}`, zone.id); } } for (const asset of ASSETS) { const created = await prisma.asset.upsert({ where: { reference: asset.reference }, update: {}, create: { reference: asset.reference, brand: asset.brand, model: asset.model, serialNumber: asset.serialNumber, commissionedAt: asset.commissionedAt ? new Date(asset.commissionedAt) : undefined, loadKg: asset.loadKg, floors: asset.floors, status: asset.status ?? 'IN_SERVICE', categoryId: categoryIds.get(asset.category)!, locationId: zoneIds.get(`${asset.site} / ${asset.zone}`)!, }, include: { _count: { select: { components: true } } }, }); if (asset.components?.length && created._count.components === 0) { await prisma.assetComponent.createMany({ data: asset.components.map((c) => ({ assetId: created.id, typeId: categoryIds.get(c.type)!, designation: c.designation, })), }); } } for (const team of TEAMS) { await prisma.team.upsert({ where: { name: team.name }, update: {}, create: { name: team.name, description: team.description, members: { connect: team.memberEmails.map((email) => ({ email })) }, }, }); } } // ————— R2 — Exploitation (référentiels du bilan, gabarits, données maquette) ————— const REFERENCE_VALUES: Record = { 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 { const refIds = new Map(); // « 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; }[] = [ { 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 })), }); } } } // ————— R3 — Gestion (tiers, pièces, mouvements, BC, taux — données maquette) ————— const PARTNERS: { name: string; kind: 'SUPPLIER' | 'CLIENT'; contactName?: string; phone?: string; email?: string; city?: string; }[] = [ { name: 'Ascentech Maroc', kind: 'SUPPLIER', contactName: 'M. Berrini', phone: '05 22 34 56 78', city: 'Casablanca' }, { name: 'SchindlerParts', kind: 'SUPPLIER', email: 'commandes@schindlerparts.ma' }, { name: 'Lubmaroc', kind: 'SUPPLIER', phone: '05 22 11 22 33' }, { name: 'Atlas Property Management', kind: 'CLIENT', contactName: 'Mme Zerhouni', phone: '06 61 98 76 54' }, { name: 'Syndic Al Manar', kind: 'CLIENT', contactName: 'M. Alami (gardien référent)' }, ]; const PARTS: { reference: string; designation: string; threshold: number; lastUnitPrice?: number; supplier?: string; compatible?: string; }[] = [ { reference: 'P-0019', designation: 'Cellule barrière porte (paire)', threshold: 4, lastUnitPrice: 640, supplier: 'SchindlerParts' }, { reference: 'P-0031', designation: 'Bouton palier lumineux Ø22', threshold: 10, lastUnitPrice: 45, supplier: 'Ascentech Maroc' }, { reference: 'P-0042', designation: 'Graisse guide (cartouche 400 g)', threshold: 8, lastUnitPrice: 85, supplier: 'Lubmaroc' }, { reference: 'P-0087', designation: 'Coulisseau de guide 16 mm', threshold: 6, lastUnitPrice: 120, supplier: 'Ascentech Maroc' }, { reference: 'P-0113', designation: 'Contact de porte NC-31', threshold: 5, lastUnitPrice: 85, supplier: 'Ascentech Maroc', compatible: 'Otis Gen2 · Schindler 3300' }, ]; async function seedGestion(prisma: PrismaClient): Promise { const partnerIds = new Map(); for (const p of PARTNERS) { const partner = await prisma.partner.upsert({ where: { name: p.name }, update: {}, create: p, }); partnerIds.set(p.name, partner.id); } const partIds = new Map(); for (const p of PARTS) { const part = await prisma.part.upsert({ where: { reference: p.reference }, update: {}, create: { reference: p.reference, designation: p.designation, threshold: p.threshold, lastUnitPrice: p.lastUnitPrice, compatible: p.compatible, supplierId: p.supplier ? partnerIds.get(p.supplier) : undefined, }, }); partIds.set(p.reference, part.id); } // Taux horaires courants (les saisies figent leur propre taux) for (const [email, taux] of [ ['technicien@demo.siop.ma', 120], ['technicien-limite@demo.siop.ma', 90], ] as const) { await prisma.user.update({ where: { email }, data: { hourlyRate: taux } }); } // Idempotence : si des mouvements existent déjà, l'histoire est en place if ((await prisma.stockMovement.count()) > 0) return; const annee = new Date().getFullYear(); const ahmed = await prisma.user.findUniqueOrThrow({ where: { email: 'technicien@demo.siop.ma' } }); const nadia = await prisma.user.findUniqueOrThrow({ where: { email: 'gestionnaire@demo.siop.ma' } }); // BC reçus (historique) et BC en cours — comme la maquette const bc21 = await prisma.purchaseOrder.create({ data: { reference: `BC-${annee}-0021`, status: 'RECEIVED', supplierId: partnerIds.get('Ascentech Maroc')!, createdById: nadia.id, sentAt: new Date(Date.now() - 40 * 86400e3), receivedAt: new Date(Date.now() - 34 * 86400e3), lines: { create: [ { partId: partIds.get('P-0113')!, quantity: 5, unitPrice: 85 }, { partId: partIds.get('P-0087')!, quantity: 6, unitPrice: 120 }, { partId: partIds.get('P-0031')!, quantity: 20, unitPrice: 45 }, ], }, }, include: { lines: true }, }); await prisma.purchaseOrder.create({ data: { reference: `BC-${annee}-0024`, status: 'SENT', supplierId: partnerIds.get('Ascentech Maroc')!, createdById: nadia.id, sentAt: new Date(), lines: { create: [ { partId: partIds.get('P-0113')!, quantity: 10, unitPrice: 85 }, { partId: partIds.get('P-0087')!, quantity: 2, unitPrice: 127.5 }, ], }, }, }); // Mouvements : réceptions du BC-0021 + entrées initiales + consommations for (const line of bc21.lines) { await prisma.stockMovement.create({ data: { partId: line.partId, kind: 'RECEIPT', quantity: line.quantity, unitPrice: line.unitPrice, purchaseOrderId: bc21.id, byId: nadia.id, createdAt: new Date(Date.now() - 34 * 86400e3), }, }); } for (const [ref, qte] of [ ['P-0042', 19], ['P-0019', 7], ['P-0031', 6], ] as const) { await prisma.stockMovement.create({ data: { partId: partIds.get(ref)!, kind: 'ENTRY', quantity: qte, reason: 'Reprise de l’inventaire initial', byId: nadia.id, createdAt: new Date(Date.now() - 60 * 86400e3), }, }); } // P-0113 : consommations + ajustement → stock 2 (sous le seuil, maquette) const ot341 = await prisma.workOrder.findUnique({ where: { reference: `OT-${annee}-0341` }, }); await prisma.stockMovement.createMany({ data: [ { partId: partIds.get('P-0113')!, kind: 'ADJUSTMENT', quantity: -1, reason: 'Inventaire — pièce endommagée', byId: nadia.id }, { partId: partIds.get('P-0113')!, kind: 'CONSUMPTION', quantity: -2, unitPrice: 85, byId: ahmed.id }, ], }); // P-0087 : consommation sur OT-0341 (carte maquette : 2 × 120 = 240 MAD) if (ot341) { await prisma.stockMovement.create({ data: { partId: partIds.get('P-0087')!, kind: 'CONSUMPTION', quantity: -2, unitPrice: 120, workOrderId: ot341.id, byId: ahmed.id, }, }); await prisma.stockMovement.create({ data: { partId: partIds.get('P-0042')!, kind: 'CONSUMPTION', quantity: -1, unitPrice: 85, workOrderId: ot341.id, byId: ahmed.id, }, }); // main-d'œuvre : 1 h 30 × 120 MAD/h = 180 (total maquette : 505 MAD) await prisma.laborTime.create({ data: { workOrderId: ot341.id, userId: ahmed.id, minutes: 90, hourlyRate: 120 }, }); } } /* c8 ignore start — wrapper CLI */ if (require.main === module) { const prisma = new PrismaClient(); seed(prisma) .then(async () => { const [roles, permissions, users] = await Promise.all([ prisma.role.count(), prisma.permission.count(), prisma.user.count({ where: { isDemo: true } }), ]); console.log( `Seed OK — ${roles} rôles, ${permissions} lignes de matrice, ${users} comptes démo.`, ); }) .catch((e) => { console.error(e); process.exitCode = 1; }) .finally(() => prisma.$disconnect()); } /* c8 ignore stop */