/** * L10a Last-Seed (Spec §34.1): füllt den Demo-Mandanten mit N Aufträgen (Default 5 000) für * Performance-Messungen der Auftragsliste und des Dashboards. * * npx tsx scripts/seed-load.ts # 5 000 Aufträge (idempotent: fehlende werden ergänzt) * LOAD_ORDERS=20000 npx tsx scripts/seed-load.ts * npx tsx scripts/seed-load.ts --reset # Last-Daten wieder entfernen * * Datenbild: 250 Kunden mit je einem Objekt, Aufträge über alle Status verteilt (Schwerpunkt offen), * Termine −90…+60 Tage, Teams Nord/Süd mit Einzelzuweisungen, je Auftrag 2 Materialvorgaben, jeder * zehnte mit Statushistorie und eingereichtem Bericht. Schnell über createMany (Owner-Client, nur * Testdaten) — fachliche Abläufe testet scripts/test-e2e-*.ts. Kennzeichen: Nummer `L-…`, * externe Auftragsnummer `LOAD-…`, Kundennummer `K-L…`. * * Messung danach: BASE=http://localhost:3110 PERF=1 npx tsx scripts/smoke-auth.ts */ import "dotenv/config"; import { randomUUID } from "node:crypto"; import { Prisma, PrismaClient, type WorkOrderStatus } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) }); const TARGET = Number(process.env.LOAD_ORDERS ?? 5000); const CUSTOMERS = 250; const CHUNK = 1000; // weighted status mix (open work dominates a real tenant) const STATUS_MIX: [WorkOrderStatus, number][] = [ ["draft", 3], ["review_required", 2], ["planned", 10], ["assigned", 12], ["accepted", 6], ["en_route", 2], ["in_progress", 8], ["paused", 2], ["waiting_material", 2], ["daily_report_created", 2], ["technically_completed", 2], ["signature_pending", 2], ["in_review", 5], ["released_for_billing", 8], ["billed", 30], ["cancelled", 4], ]; const WEIGHT = STATUS_MIX.reduce((s, [, w]) => s + w, 0); function statusFor(i: number): WorkOrderStatus { let x = (i * 7919) % WEIGHT; for (const [s, w] of STATUS_MIX) { if (x < w) return s; x -= w; } return "planned"; } async function reset(tenantId: string) { const orders = await prisma.workOrder.findMany({ where: { tenantId, externalOrderNumber: { startsWith: "LOAD-" } }, select: { id: true } }); const ids = orders.map((o) => o.id); for (let i = 0; i < ids.length; i += CHUNK) { const w = { workOrderId: { in: ids.slice(i, i + CHUNK) } }; await prisma.report.deleteMany({ where: w }); await prisma.workOrderStatusChange.deleteMany({ where: w }); await prisma.workOrder.deleteMany({ where: { id: { in: ids.slice(i, i + CHUNK) } } }); } await prisma.site.deleteMany({ where: { tenantId, customer: { customerNumber: { startsWith: "K-L" } } } }); const c = await prisma.customer.deleteMany({ where: { tenantId, customerNumber: { startsWith: "K-L" } } }); console.log(`✔ Last-Daten entfernt: ${ids.length} Aufträge, ${c.count} Kunden`); } async function main() { const tenant = await prisma.tenant.findUnique({ where: { slug: "demo" } }); if (!tenant) throw new Error('Mandant "demo" fehlt — zuerst `npx prisma db seed`.'); const tenantId = tenant.id; if (process.argv.includes("--reset")) return reset(tenantId); const teams = await prisma.team.findMany({ where: { tenantId, name: { in: ["Team Nord", "Team Süd"] } }, include: { members: true } }); if (teams.length < 2) throw new Error("Demo-Teams fehlen — Seed mit Demo-Daten ausführen (SEED_DEMO)."); const orderTypes = await prisma.orderType.findMany({ where: { tenantId }, select: { id: true } }); const creator = await prisma.user.findFirstOrThrow({ where: { tenantId, email: "backoffice@demo.example" } }); // customers + sites const existingCustomers = await prisma.customer.count({ where: { tenantId, customerNumber: { startsWith: "K-L" } } }); if (existingCustomers < CUSTOMERS) { const rows = Array.from({ length: CUSTOMERS }, (_, i) => ({ id: randomUUID(), tenantId, customerNumber: `K-L${String(i + 1).padStart(4, "0")}`, companyName: `Lastkunde ${String(i + 1).padStart(3, "0")} GmbH`, street: "Lastweg", houseNumber: String(i + 1), postalCode: String(20000 + (i % 900)), city: i % 2 ? "Hamburg" : "Lübeck", })); await prisma.customer.createMany({ data: rows, skipDuplicates: true }); const customers = await prisma.customer.findMany({ where: { tenantId, customerNumber: { startsWith: "K-L" }, sites: { none: {} } }, select: { id: true, customerNumber: true } }); await prisma.site.createMany({ data: customers.map((c) => ({ tenantId, customerId: c.id, name: `Objekt ${c.customerNumber}`, street: "Lastweg", city: "Hamburg", accessNotes: "Schlüssel beim Hausmeister" })) }); } const customers = await prisma.customer.findMany({ where: { tenantId, customerNumber: { startsWith: "K-L" } }, select: { id: true, sites: { select: { id: true }, take: 1 } }, orderBy: { customerNumber: "asc" } }); const existing = await prisma.workOrder.count({ where: { tenantId, externalOrderNumber: { startsWith: "LOAD-" } } }); const missing = Math.max(0, TARGET - existing); console.log(`Mandant demo: ${existing} Last-Aufträge vorhanden, lege ${missing} an …`); const t0 = Date.now(); const day = 86400_000; for (let offset = existing; offset < existing + missing; offset += CHUNK) { const n = Math.min(CHUNK, existing + missing - offset); const orders: Prisma.WorkOrderCreateManyInput[] = []; const assignees: Prisma.WorkOrderAssigneeCreateManyInput[] = []; const plans: Prisma.MaterialPlanCreateManyInput[] = []; const history: Prisma.WorkOrderStatusChangeCreateManyInput[] = []; const reports: Prisma.ReportCreateManyInput[] = []; for (let k = 0; k < n; k++) { const i = offset + k; const status = statusFor(i); const team = teams[i % 2]; const customer = customers[i % customers.length]; const start = new Date(Date.now() + (((i * 37) % 150) - 90) * day + ((i % 9) + 7) * 3600_000); const id = randomUUID(); const planned = !["draft", "review_required"].includes(status); orders.push({ id, tenantId, number: `L-${String(i + 1).padStart(6, "0")}`, externalOrderNumber: `LOAD-${i + 1}`, customerId: customer.id, siteId: customer.sites[0]?.id ?? null, orderTypeId: orderTypes.length ? orderTypes[i % orderTypes.length].id : null, priority: i % 17 === 0 ? "urgent" : i % 5 === 0 ? "high" : "normal", status, title: `Lastauftrag ${i + 1}: ${["Wartung Heizung", "Reparatur Leitung", "Montage Therme", "Störung Lüftung"][i % 4]}`, description: "Automatisch erzeugter Auftrag für Lasttests.", plannedStart: start, plannedEnd: new Date(start.getTime() + 4 * 3600_000), assignedTeamId: planned ? team.id : null, teamLeadUserId: planned ? team.leaderUserId : null, isEmergency: i % 97 === 0, createdById: creator.id, createdAt: new Date(start.getTime() - 14 * day), }); const member = team.members[i % Math.max(1, team.members.length)]; if (planned && member) assignees.push({ tenantId, workOrderId: id, userId: member.userId }); plans.push( { tenantId, workOrderId: id, name: "Kupferrohr 15 mm", plannedQuantity: new Prisma.Decimal(10), unit: "m" }, { tenantId, workOrderId: id, name: "Pressfitting 15 mm", plannedQuantity: new Prisma.Decimal(6), unit: "Stk", sortOrder: 1 }, ); if (i % 10 === 0) { history.push({ tenantId, workOrderId: id, fromStatus: null, toStatus: "planned", actorId: creator.id }, { tenantId, workOrderId: id, fromStatus: "planned", toStatus: status, actorId: creator.id }); if (["in_review", "released_for_billing", "billed"].includes(status)) { reports.push({ tenantId, workOrderId: id, type: "completion", reportDate: start, lineageId: randomUUID(), status: status === "in_review" ? "submitted" : "approved", content: {}, createdById: creator.id }); } } } await prisma.workOrder.createMany({ data: orders }); await prisma.workOrderAssignee.createMany({ data: assignees, skipDuplicates: true }); await prisma.materialPlan.createMany({ data: plans }); if (history.length) await prisma.workOrderStatusChange.createMany({ data: history }); if (reports.length) await prisma.report.createMany({ data: reports }); process.stdout.write(` ${offset + n - existing}/${missing}\r`); } await prisma.$executeRawUnsafe("ANALYZE work_orders, work_order_assignees, material_plans, reports, customers, sites"); const total = await prisma.workOrder.count({ where: { tenantId } }); console.log(`\n✔ ${missing} Last-Aufträge in ${((Date.now() - t0) / 1000).toFixed(1)} s angelegt — Mandant demo hat jetzt ${total} Aufträge.`); } main() .then(() => prisma.$disconnect()) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });