160 lines
8.8 KiB
TypeScript
160 lines
8.8 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|