Files
craftvia/scripts/seed-load.ts
T

160 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});