L10a Qualität & Abnahmetests: Demo-Seed (Teams, Kunden, Objekte, 20 Aufträge, Berichte, Notdienst, demo2)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+36
-3
@@ -8,7 +8,9 @@ import { provisionTenant } from "../src/server/provision";
|
|||||||
/**
|
/**
|
||||||
* Craftvia-Demo-Seed (Fundament): Plattform-Admin + Mandant `demo` mit je einem Nutzer
|
* Craftvia-Demo-Seed (Fundament): Plattform-Admin + Mandant `demo` mit je einem Nutzer
|
||||||
* pro Standardrolle + zweiter Mandant `demo2` (Isolations-/Tenant-Switch-Tests) +
|
* pro Standardrolle + zweiter Mandant `demo2` (Isolations-/Tenant-Switch-Tests) +
|
||||||
* Multi-Membership-Fixture. KEINE Fachdaten (Kunden/Aufträge liefern die Module).
|
* Multi-Membership-Fixture. Fachliche Demo-Daten (Teams, Kunden, Objekte, Aufträge, Berichte,
|
||||||
|
* Notdienst) kommen aus scripts/lib/demo-seed.ts — nur mit SEED_DEMO=true oder außerhalb von
|
||||||
|
* Production (SEED_DEMO=false schaltet sie ab).
|
||||||
*
|
*
|
||||||
* Idempotent (upserts) — beliebig oft ausführbar. Passwort: SEED_PASSWORD oder Default.
|
* Idempotent (upserts) — beliebig oft ausführbar. Passwort: SEED_PASSWORD oder Default.
|
||||||
*/
|
*/
|
||||||
@@ -66,8 +68,13 @@ async function main() {
|
|||||||
{ email: "backoffice@demo.example", name: "Bernd Backoffice", roles: ["backoffice"] },
|
{ email: "backoffice@demo.example", name: "Bernd Backoffice", roles: ["backoffice"] },
|
||||||
{ email: "teamleiter@demo.example", name: "Tina Teamleiter", roles: ["team-lead"] },
|
{ email: "teamleiter@demo.example", name: "Tina Teamleiter", roles: ["team-lead"] },
|
||||||
{ email: "monteur@demo.example", name: "Max Monteur", roles: ["technician"] },
|
{ email: "monteur@demo.example", name: "Max Monteur", roles: ["technician"] },
|
||||||
|
// L10a Demo-Teams: Team Nord (Tina + Max + Nora), Team Süd (Sven + Paul)
|
||||||
|
{ email: "teamleiter2@demo.example", name: "Sven Südfeld", roles: ["team-lead"] },
|
||||||
|
{ email: "monteur2@demo.example", name: "Nora Nordmann", roles: ["technician"] },
|
||||||
|
{ email: "monteur3@demo.example", name: "Paul Petersen", roles: ["technician"] },
|
||||||
];
|
];
|
||||||
for (const u of demoUsers) await ensureMember(demo.id, u, passwordHash);
|
const members: Record<string, string> = {};
|
||||||
|
for (const u of demoUsers) members[u.email] = (await ensureMember(demo.id, u, passwordHash)).id;
|
||||||
console.log(`✔ Mandant "demo" (${PERMISSIONS.length} Permissions) mit ${demoUsers.length} Nutzern (Passwort: ${DEMO_PASSWORD})`);
|
console.log(`✔ Mandant "demo" (${PERMISSIONS.length} Permissions) mit ${demoUsers.length} Nutzern (Passwort: ${DEMO_PASSWORD})`);
|
||||||
|
|
||||||
// 2. Zweiter Mandant „demo2" — Grundlage für Isolations- und Tenant-Switch-Tests.
|
// 2. Zweiter Mandant „demo2" — Grundlage für Isolations- und Tenant-Switch-Tests.
|
||||||
@@ -93,10 +100,36 @@ async function main() {
|
|||||||
create: { email: "platform@demo.example", name: "Paula Plattform", passwordHash, status: "ACTIVE" },
|
create: { email: "platform@demo.example", name: "Paula Plattform", passwordHash, status: "ACTIVE" },
|
||||||
});
|
});
|
||||||
console.log(`✔ Plattform-Admin platform@demo.example (/platform/login)`);
|
console.log(`✔ Plattform-Admin platform@demo.example (/platform/login)`);
|
||||||
|
|
||||||
|
// 5. L10a: fachliche Demo-Daten (Teams, Kunden, Objekte, 20 Aufträge, Berichte, Notdienst).
|
||||||
|
// Nur mit SEED_DEMO=true oder außerhalb von Production; SEED_DEMO=false schaltet sie ab.
|
||||||
|
const demoData = process.env.SEED_DEMO === "true" || (process.env.NODE_ENV !== "production" && process.env.SEED_DEMO !== "false");
|
||||||
|
if (demoData) {
|
||||||
|
const { seedDemoTenant, seedDemo2Tenant, closeDemoSeed } = await import("../scripts/lib/demo-seed");
|
||||||
|
const res = await seedDemoTenant(demo.id, {
|
||||||
|
admin: members["admin@demo.example"],
|
||||||
|
backoffice: members["backoffice@demo.example"],
|
||||||
|
lead: members["teamleiter@demo.example"],
|
||||||
|
lead2: members["teamleiter2@demo.example"],
|
||||||
|
tech: members["monteur@demo.example"],
|
||||||
|
tech2: members["monteur2@demo.example"],
|
||||||
|
tech3: members["monteur3@demo.example"],
|
||||||
|
});
|
||||||
|
const admin2 = await prisma.user.findUniqueOrThrow({ where: { tenantId_email: { tenantId: demo2.id, email: "admin2@demo.example" } } });
|
||||||
|
await seedDemo2Tenant(demo2.id, admin2.id);
|
||||||
|
await closeDemoSeed();
|
||||||
|
console.log(`✔ Demo-Fachdaten: ${res.orders} Demo-Aufträge in "demo" (${res.created} neu angelegt), 1 Auftrag in "demo2"`);
|
||||||
|
} else {
|
||||||
|
console.log("• Demo-Fachdaten übersprungen (SEED_DEMO=false bzw. Production ohne SEED_DEMO=true)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
.then(() => prisma.$disconnect())
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
// Services may leave queue/redis handles open (events, image jobs) — end the seed explicitly.
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
// L10a Demo-Seed: fachliche Demo-Daten für die Mandanten `demo` und `demo2`.
|
||||||
|
//
|
||||||
|
// Wird von prisma/seed.ts aufgerufen (nur mit SEED_DEMO=true oder außerhalb von Production).
|
||||||
|
// Alle Datensätze entstehen über die ECHTEN Fachservices (Statusmaschine, Zuweisung, Sessions,
|
||||||
|
// Berichte, Notdienst) – damit sind Statushistorie, Audit, Versionen und Berichts-Snapshots
|
||||||
|
// konsistent wie im Echtbetrieb. Idempotent: Kunden über feste Kundennummern (K-D…), Objekte über
|
||||||
|
// Kunde + Name, Aufträge über die externe Auftragsnummer DEMO-…, Notdienst über feste Client-IDs.
|
||||||
|
// Alle Namen, Adressen und Telefonnummern sind frei erfunden (keine echten personenbezogenen Daten).
|
||||||
|
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { prisma, dbForTenant } from "../../src/server/db";
|
||||||
|
import { ROLE_DEFS, type RoleKey } from "../../src/server/rbac";
|
||||||
|
import type { ServiceCtx } from "../../src/server/services/context";
|
||||||
|
import { localDateKey } from "../../src/lib/reports/dates";
|
||||||
|
import {
|
||||||
|
checklistTogglePayload,
|
||||||
|
materialUpsertPayload,
|
||||||
|
noteCreatePayload,
|
||||||
|
photoAttachPayload,
|
||||||
|
sessionControlPayload,
|
||||||
|
sessionStartPayload,
|
||||||
|
type NoteKind,
|
||||||
|
} from "../../src/lib/sync/ops";
|
||||||
|
import { createCustomer } from "../../src/server/services/customers/customers";
|
||||||
|
import { createContact } from "../../src/server/services/customers/contacts";
|
||||||
|
import { createSite } from "../../src/server/services/sites/sites";
|
||||||
|
import { createTeam } from "../../src/server/services/teams/teams";
|
||||||
|
import { createChecklistTemplate, ensureDefaultOrderTypes } from "../../src/server/services/work-orders/settings";
|
||||||
|
import { createWorkOrder } from "../../src/server/services/work-orders/create";
|
||||||
|
import { assignWorkOrder } from "../../src/server/services/work-orders/assign";
|
||||||
|
import { transitionWorkOrder } from "../../src/server/services/work-orders/transition";
|
||||||
|
import { markBilled, releaseForBilling } from "../../src/server/services/work-orders/release-billing";
|
||||||
|
import { endSession, pauseSession, startSession } from "../../src/server/services/field/sessions";
|
||||||
|
import { createNote } from "../../src/server/services/field/notes";
|
||||||
|
import { upsertMaterialUsage } from "../../src/server/services/field/materials";
|
||||||
|
import { toggleChecklistItem } from "../../src/server/services/field/checklist";
|
||||||
|
import { storeFieldUpload } from "../../src/server/services/field/uploads";
|
||||||
|
import { attachPhoto } from "../../src/server/services/field/photos";
|
||||||
|
import { storeFile } from "../../src/server/services/documents/store";
|
||||||
|
import { createCompletionReport, createDailyReport } from "../../src/server/services/reports/create";
|
||||||
|
import { updateReportTexts } from "../../src/server/services/reports/edit";
|
||||||
|
import { captureSignature } from "../../src/server/services/reports/signature";
|
||||||
|
import { submitReport } from "../../src/server/services/reports/submit";
|
||||||
|
import { approveReport } from "../../src/server/services/reports/approve";
|
||||||
|
import { createEmergencyOrder } from "../../src/server/services/emergency/create";
|
||||||
|
import type { CreateWorkOrderInput } from "../../src/lib/work-orders/schemas";
|
||||||
|
|
||||||
|
export type DemoUsers = Record<"admin" | "backoffice" | "lead" | "lead2" | "tech" | "tech2" | "tech3", string>;
|
||||||
|
|
||||||
|
const TZ = "Europe/Berlin";
|
||||||
|
const noPdf = { dispatchPdf: async () => undefined };
|
||||||
|
|
||||||
|
function ctxOf(tenantId: string, userId: string, role: RoleKey): ServiceCtx {
|
||||||
|
return { db: dbForTenant(tenantId), tenantId, userId, permissions: new Set<string>(ROLE_DEFS[role].permissions) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local date relative to today (server time zone), e.g. day(-1, 7, 30) = yesterday 07:30. */
|
||||||
|
function day(offset: number, h = 8, m = 0): Date {
|
||||||
|
const x = new Date();
|
||||||
|
x.setDate(x.getDate() + offset);
|
||||||
|
x.setHours(h, m, 0, 0);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function demoJpeg(label: string): Promise<Buffer> {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="768"><rect width="1024" height="768" fill="#dfe6ee"/><rect x="80" y="120" width="864" height="528" fill="#9fb3c8"/><text x="512" y="400" font-size="56" font-family="sans-serif" text-anchor="middle" fill="#1f2d3d">${label}</text></svg>`;
|
||||||
|
return sharp(Buffer.from(svg)).jpeg({ quality: 80 }).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signaturePng(): Promise<Buffer> {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="600" height="200"><rect width="600" height="200" fill="#ffffff"/><path d="M40 140 C 90 40, 140 180, 190 110 S 290 60, 330 130 S 450 90, 560 120" stroke="#1f2d3d" stroke-width="4" fill="none"/></svg>`;
|
||||||
|
return sharp(Buffer.from(svg)).png().toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedDemoTenant(tenantId: string, users: DemoUsers): Promise<{ orders: number; created: number }> {
|
||||||
|
const admin = ctxOf(tenantId, users.admin, "tenant-admin");
|
||||||
|
const bo = ctxOf(tenantId, users.backoffice, "backoffice");
|
||||||
|
const lead = ctxOf(tenantId, users.lead, "team-lead");
|
||||||
|
const lead2 = ctxOf(tenantId, users.lead2, "team-lead");
|
||||||
|
const tech = ctxOf(tenantId, users.tech, "technician");
|
||||||
|
const tech2 = ctxOf(tenantId, users.tech2, "technician");
|
||||||
|
const tech3 = ctxOf(tenantId, users.tech3, "technician");
|
||||||
|
|
||||||
|
// ---------- Auftragsarten + Checklisten-Vorlagen ----------
|
||||||
|
await ensureDefaultOrderTypes(admin.db, tenantId);
|
||||||
|
const orderTypes = new Map((await admin.db.orderType.findMany({ select: { id: true, key: true } })).map((t) => [t.key, t.id]));
|
||||||
|
const type = (key: string) => orderTypes.get(key) ?? null;
|
||||||
|
|
||||||
|
const templates: Parameters<typeof createChecklistTemplate>[1][] = [
|
||||||
|
{
|
||||||
|
name: "Montage Heizung/Sanitär",
|
||||||
|
orderTypeId: type("montage"),
|
||||||
|
items: [
|
||||||
|
{ key: "arbeitsbereich_abgesichert", label: "Arbeitsbereich abgesichert", required: true },
|
||||||
|
{ key: "material_geprueft", label: "Material geprüft", required: false },
|
||||||
|
{ key: "dichtheit_geprueft", label: "Dichtheitsprüfung durchgeführt", required: true },
|
||||||
|
{ key: "funktionspruefung", label: "Funktionsprüfung durchgeführt", required: true },
|
||||||
|
{ key: "kunde_eingewiesen", label: "Kunde eingewiesen", required: false },
|
||||||
|
],
|
||||||
|
requiredPhotos: [
|
||||||
|
{ key: "ausgangszustand", label: "Ausgangszustand" },
|
||||||
|
{ key: "typenschild", label: "Typenschild" },
|
||||||
|
{ key: "fertige_montage", label: "Fertige Montage" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wartung Heizungsanlage",
|
||||||
|
orderTypeId: type("wartung"),
|
||||||
|
items: [
|
||||||
|
{ key: "brenner_gereinigt", label: "Brenner gereinigt", required: true },
|
||||||
|
{ key: "abgaswerte_gemessen", label: "Abgaswerte gemessen", required: true },
|
||||||
|
{ key: "anlagendruck_geprueft", label: "Anlagendruck geprüft", required: true },
|
||||||
|
],
|
||||||
|
requiredPhotos: [{ key: "typenschild", label: "Typenschild" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const tpl of templates) {
|
||||||
|
if (!(await admin.db.checklistTemplate.findFirst({ where: { name: tpl.name }, select: { id: true } }))) await createChecklistTemplate(admin, tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Teams ----------
|
||||||
|
async function team(name: string, leaderUserId: string, memberIds: string[], extra: { vehicle: string; area: string; phone: string }) {
|
||||||
|
const existing = await admin.db.team.findFirst({ where: { name, deletedAt: null }, select: { id: true } });
|
||||||
|
if (existing) return existing.id;
|
||||||
|
const validFrom = day(-120, 0, 0).toISOString();
|
||||||
|
return (await createTeam(admin, { name, leaderUserId, ...extra, members: memberIds.map((userId) => ({ userId, validFrom })) })).id;
|
||||||
|
}
|
||||||
|
const nord = await team("Team Nord", users.lead, [users.tech, users.tech2], { vehicle: "HH-MB 101", area: "Hamburg Nord, Norderstedt", phone: "040 555 0110" });
|
||||||
|
const sued = await team("Team Süd", users.lead2, [users.tech3], { vehicle: "HH-MB 202", area: "Hamburg Süd, Harburg", phone: "040 555 0120" });
|
||||||
|
|
||||||
|
// ---------- Kunden, Ansprechpartner, Objekte ----------
|
||||||
|
type CustomerSeed = { number: string; companyName?: string; firstName?: string; lastName?: string; street: string; houseNumber: string; postalCode: string; city: string; phone: string; email?: string; contact?: { name: string; role: string; phone: string } };
|
||||||
|
const customerSeeds: CustomerSeed[] = [
|
||||||
|
{ number: "K-D001", companyName: "Hausverwaltung Elbblick GmbH", street: "Große Elbstraße", houseNumber: "140", postalCode: "22767", city: "Hamburg", phone: "040 555 0201", email: "service@elbblick-hv.example", contact: { name: "Frauke Albers", role: "Objektbetreuung", phone: "040 555 0202" } },
|
||||||
|
{ number: "K-D002", companyName: "Bäckerei Morgenrot KG", street: "Osterstraße", houseNumber: "38", postalCode: "20259", city: "Hamburg", phone: "040 555 0211", contact: { name: "Hannes Korn", role: "Inhaber", phone: "0171 555 0212" } },
|
||||||
|
{ number: "K-D003", companyName: "Wohnbau Alstertal eG", street: "Wellingsbütteler Landstraße", houseNumber: "7", postalCode: "22391", city: "Hamburg", phone: "040 555 0221", email: "technik@alstertal-eg.example", contact: { name: "Deniz Yilmaz", role: "Technik", phone: "040 555 0222" } },
|
||||||
|
{ number: "K-D004", companyName: "Praxis am Stadtpark", street: "Jarrestraße", houseNumber: "12", postalCode: "22303", city: "Hamburg", phone: "040 555 0231" },
|
||||||
|
{ number: "K-D005", firstName: "Greta", lastName: "Musterfrau", street: "Lindenallee", houseNumber: "4", postalCode: "21073", city: "Hamburg", phone: "040 555 0241", email: "g.musterfrau@example.org" },
|
||||||
|
{ number: "K-D006", companyName: "Logistikpark Harburg GmbH", street: "Seehafenstraße", houseNumber: "20", postalCode: "21079", city: "Hamburg", phone: "040 555 0251", contact: { name: "Sören Petersen", role: "Facility Management", phone: "0160 555 0252" } },
|
||||||
|
{ number: "K-D007", companyName: "Kita Sonnenblume e. V.", street: "Hallerstraße", houseNumber: "55", postalCode: "20146", city: "Hamburg", phone: "040 555 0261" },
|
||||||
|
];
|
||||||
|
const customers = new Map<string, { id: string; contactId: string | null }>();
|
||||||
|
for (const c of customerSeeds) {
|
||||||
|
let row = await bo.db.customer.findFirst({ where: { customerNumber: c.number }, select: { id: true } });
|
||||||
|
if (!row) {
|
||||||
|
row = await createCustomer(
|
||||||
|
bo,
|
||||||
|
{ customerNumber: c.number, companyName: c.companyName, firstName: c.firstName, lastName: c.lastName, street: c.street, houseNumber: c.houseNumber, postalCode: c.postalCode, city: c.city, phone: c.phone, email: c.email, notes: "Demo-Datensatz" },
|
||||||
|
{ acknowledgeDuplicates: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let contactId: string | null = null;
|
||||||
|
if (c.contact) {
|
||||||
|
const existing = await bo.db.contact.findFirst({ where: { customerId: row.id, name: c.contact.name }, select: { id: true } });
|
||||||
|
contactId = existing?.id ?? (await createContact(bo, row.id, { ...c.contact, preferredChannel: "phone" })).id;
|
||||||
|
}
|
||||||
|
customers.set(c.number, { id: row.id, contactId });
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiteSeed = { customer: string; name: string; street: string; houseNumber: string; postalCode: string; city: string; accessNotes: string; parkingNotes?: string; safetyNotes?: string; technicalNotes?: string };
|
||||||
|
const siteSeeds: SiteSeed[] = [
|
||||||
|
{ customer: "K-D001", name: "Wohnanlage Elbblick – Haus A", street: "Große Elbstraße", houseNumber: "140a", postalCode: "22767", city: "Hamburg", accessNotes: "Schlüssel im Schlüsseltresor neben dem Eingang, Code beim Büro erfragen.", parkingNotes: "Besucherparkplätze im Hof.", technicalNotes: "Heizungsraum im Keller, Gas-Brennwertkessel Baujahr 2014." },
|
||||||
|
{ customer: "K-D001", name: "Wohnanlage Elbblick – Haus B", street: "Große Elbstraße", houseNumber: "140b", postalCode: "22767", city: "Hamburg", accessNotes: "Hausmeister Tel. 040 555 0203 (Mo–Fr 7–15 Uhr).", safetyNotes: "Asbestverdacht im Altbau-Keller – vor Bohrarbeiten Büro informieren." },
|
||||||
|
{ customer: "K-D002", name: "Backstube Osterstraße", street: "Osterstraße", houseNumber: "38", postalCode: "20259", city: "Hamburg", accessNotes: "Lieferanteneingang hinten, ab 4 Uhr besetzt.", parkingNotes: "Ladezone vor dem Haus maximal 30 Minuten." },
|
||||||
|
{ customer: "K-D002", name: "Filiale Eppendorf", street: "Eppendorfer Baum", houseNumber: "9", postalCode: "20249", city: "Hamburg", accessNotes: "Nur außerhalb der Öffnungszeiten (ab 18 Uhr).", technicalNotes: "Wasseraufbereitung unter der Theke." },
|
||||||
|
{ customer: "K-D003", name: "Quartier Poppenbüttel – Block 3", street: "Saseler Damm", houseNumber: "21", postalCode: "22391", city: "Hamburg", accessNotes: "Zugang über Tiefgarage, Transponder beim Hausmeister.", safetyNotes: "Dachzugang nur mit Absturzsicherung." },
|
||||||
|
{ customer: "K-D003", name: "Quartier Poppenbüttel – Block 5", street: "Saseler Damm", houseNumber: "25", postalCode: "22391", city: "Hamburg", accessNotes: "Hauptschlüssel im Büro der eG abholen." },
|
||||||
|
{ customer: "K-D004", name: "Praxisräume 2. OG", street: "Jarrestraße", houseNumber: "12", postalCode: "22303", city: "Hamburg", accessNotes: "Anmeldung am Empfang, Arbeiten nur mittwochs nachmittags.", safetyNotes: "Röntgenraum nicht betreten." },
|
||||||
|
{ customer: "K-D005", name: "Einfamilienhaus Lindenallee", street: "Lindenallee", houseNumber: "4", postalCode: "21073", city: "Hamburg", accessNotes: "Kundin ist vormittags zu Hause, Hund im Garten.", parkingNotes: "Einfahrt frei halten." },
|
||||||
|
{ customer: "K-D006", name: "Halle 1 – Kühllager", street: "Seehafenstraße", houseNumber: "20", postalCode: "21079", city: "Hamburg", accessNotes: "Anmeldung an der Pforte, Besucherausweis erforderlich.", safetyNotes: "Warnweste und Sicherheitsschuhe Pflicht, Staplerverkehr." },
|
||||||
|
{ customer: "K-D006", name: "Verwaltungsgebäude", street: "Seehafenstraße", houseNumber: "22", postalCode: "21079", city: "Hamburg", accessNotes: "Haustechnikraum im EG, Schlüssel an der Pforte.", technicalNotes: "Lüftungsanlage mit Wärmerückgewinnung." },
|
||||||
|
{ customer: "K-D007", name: "Kita-Gebäude Hallerstraße", street: "Hallerstraße", houseNumber: "55", postalCode: "20146", city: "Hamburg", accessNotes: "Nur nach Absprache mit der Leitung, Kinder im Haus.", safetyNotes: "Werkzeug nie unbeaufsichtigt lassen." },
|
||||||
|
{ customer: "K-D007", name: "Gartenhaus / Außenanlage", street: "Hallerstraße", houseNumber: "55", postalCode: "20146", city: "Hamburg", accessNotes: "Tor zum Garten mit Zahlenschloss, Code im Auftrag." },
|
||||||
|
];
|
||||||
|
const sites = new Map<string, string>();
|
||||||
|
for (const s of siteSeeds) {
|
||||||
|
const customer = customers.get(s.customer)!;
|
||||||
|
const existing = await bo.db.site.findFirst({ where: { customerId: customer.id, name: s.name, deletedAt: null }, select: { id: true } });
|
||||||
|
const { customer: _c, ...fields } = s;
|
||||||
|
void _c;
|
||||||
|
sites.set(s.name, existing?.id ?? (await createSite(bo, { ...fields, customerId: customer.id, contactId: customer.contactId })).id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer für den Einsatz ----------
|
||||||
|
let created = 0;
|
||||||
|
const jpeg = await demoJpeg("Demo-Foto");
|
||||||
|
const signature = await signaturePng();
|
||||||
|
|
||||||
|
const at = (d: Date) => d.toISOString();
|
||||||
|
const work = (c: ServiceCtx, id: string, d: Date) => startSession(c, sessionStartPayload.parse({ workOrderId: id, mode: "work", at: at(d), deviceInfo: "Demo-Seed" }));
|
||||||
|
const travel = (c: ServiceCtx, id: string, d: Date) => startSession(c, sessionStartPayload.parse({ workOrderId: id, mode: "travel", at: at(d), deviceInfo: "Demo-Seed" }));
|
||||||
|
const pause = (c: ServiceCtx, id: string, d: Date) => pauseSession(c, sessionControlPayload.parse({ workOrderId: id, at: at(d) }));
|
||||||
|
const end = (c: ServiceCtx, id: string, d: Date) => endSession(c, sessionControlPayload.parse({ workOrderId: id, at: at(d) }));
|
||||||
|
const note = (c: ServiceCtx, id: string, kind: NoteKind, text: string) => createNote(c, noteCreatePayload.parse({ workOrderId: id, kind, text }));
|
||||||
|
const checkAll = async (c: ServiceCtx, id: string) => {
|
||||||
|
for (const item of await c.db.checklistItem.findMany({ where: { workOrderId: id, checked: false }, select: { id: true } })) {
|
||||||
|
await toggleChecklistItem(c, checklistTogglePayload.parse({ workOrderId: id, itemId: item.id, checked: true }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const photo = async (c: ServiceCtx, id: string, opts: { phase: "before" | "during" | "after"; requirementKey?: string; comment?: string; takenAt?: Date }) => {
|
||||||
|
const upload = await storeFieldUpload(c, { clientId: randomUUID(), workOrderId: id, kind: "photo" }, { bytes: jpeg, name: `${opts.requirementKey ?? opts.phase}.jpg`, type: "image/jpeg" }, { bytes: jpeg, name: "thumb.jpg", type: "image/jpeg" });
|
||||||
|
const req = opts.requirementKey ? await c.db.photoRequirement.findFirst({ where: { workOrderId: id, key: opts.requirementKey }, select: { id: true } }) : null;
|
||||||
|
await attachPhoto(c, photoAttachPayload.parse({ workOrderId: id, documentId: upload.documentId, phase: opts.phase, photoRequirementId: req?.id ?? null, comment: opts.comment, takenAt: opts.takenAt ? at(opts.takenAt) : undefined }));
|
||||||
|
};
|
||||||
|
const material = (c: ServiceCtx, id: string, input: Record<string, unknown>) => upsertMaterialUsage(c, materialUpsertPayload.parse({ workOrderId: id, ...input }));
|
||||||
|
const planMaterial = async (c: ServiceCtx, id: string, index: number, quantity: number, usageStatus: "fully_used" | "partially_used" | "not_used", deviationReason?: string) => {
|
||||||
|
const plan = (await c.db.materialPlan.findMany({ where: { workOrderId: id }, orderBy: { sortOrder: "asc" }, select: { id: true, unit: true } }))[index];
|
||||||
|
if (plan) await material(c, id, { materialPlanId: plan.id, quantity, unit: plan.unit, usageStatus, deviationReason });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Completion report with signature outcome and optional approvals (team lead → backoffice). */
|
||||||
|
const complete = async (
|
||||||
|
c: ServiceCtx,
|
||||||
|
id: string,
|
||||||
|
opts: { reportDate: Date; workPerformed: string; outcome: "signed" | "customer_absent" | "later"; signer?: string; approve: "none" | "team" | "final"; teamLead: ServiceCtx },
|
||||||
|
) => {
|
||||||
|
const { report } = await createCompletionReport(c, { workOrderId: id, reportDate: localDateKey(opts.reportDate, TZ) });
|
||||||
|
await updateReportTexts(c, { reportId: report.id, texts: { workPerformed: opts.workPerformed, hints: "Anlage läuft störungsfrei.", nextSteps: "Nächste Wartung in 12 Monaten." } });
|
||||||
|
const confirmationText = "Ich bestätige die Ausführung der aufgeführten Arbeiten.";
|
||||||
|
if (opts.outcome === "signed") {
|
||||||
|
const img = await storeFile(c, { bytes: signature, fileName: "unterschrift.png", declaredMime: "image/png", category: "signature", visibility: "customer_report", links: { workOrderId: id } });
|
||||||
|
await captureSignature(c, { reportId: report.id, outcome: "signed", signerName: opts.signer ?? "Frauke Albers", signerRole: "Objektbetreuung", imageDocumentId: img.id, confirmationText });
|
||||||
|
} else {
|
||||||
|
await captureSignature(c, { reportId: report.id, outcome: opts.outcome, reason: opts.outcome === "later" ? "Ansprechpartnerin nicht vor Ort, Unterschrift wird nachgereicht." : "Kunde trotz Terminabsprache nicht angetroffen.", confirmationText });
|
||||||
|
}
|
||||||
|
await submitReport(c, { reportId: report.id });
|
||||||
|
if (opts.approve !== "none") await approveReport(opts.teamLead, { reportId: report.id }, noPdf);
|
||||||
|
if (opts.approve === "final") await approveReport(bo, { reportId: report.id }, noPdf);
|
||||||
|
return report.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrderSeed = Omit<CreateWorkOrderInput, "customerId" | "siteId" | "contactId" | "externalOrderNumber"> & { ext: string; customer: string; site?: string };
|
||||||
|
const order = async (seed: OrderSeed, drive: (id: string) => Promise<void>) => {
|
||||||
|
const existing = await bo.db.workOrder.findFirst({ where: { externalOrderNumber: seed.ext }, select: { id: true } });
|
||||||
|
if (existing) return existing.id;
|
||||||
|
const { ext, customer, site, ...input } = seed;
|
||||||
|
const cust = customers.get(customer)!;
|
||||||
|
const wo = await createWorkOrder(bo, { ...input, externalOrderNumber: ext, customerId: cust.id, siteId: site ? sites.get(site)! : null, contactId: cust.contactId });
|
||||||
|
await drive(wo.id);
|
||||||
|
created++;
|
||||||
|
return wo.id;
|
||||||
|
};
|
||||||
|
const assign = (id: string, teamId: string, userIds: string[]) => assignWorkOrder(bo, { workOrderId: id, teamId, userIds });
|
||||||
|
|
||||||
|
const heating = [
|
||||||
|
{ name: "Gas-Brennwerttherme 24 kW", articleNumber: "GBT-24", plannedQuantity: 1, unit: "Stk" },
|
||||||
|
{ name: "Kupferrohr 18 mm", articleNumber: "CU-18", plannedQuantity: 12, unit: "m" },
|
||||||
|
{ name: "Pressfitting 18 mm", articleNumber: "PF-18", plannedQuantity: 10, unit: "Stk" },
|
||||||
|
];
|
||||||
|
const service = [
|
||||||
|
{ name: "Wartungsset Brenner", articleNumber: "WS-B1", plannedQuantity: 1, unit: "Satz" },
|
||||||
|
{ name: "Dichtungssatz", articleNumber: "DS-12", plannedQuantity: 2, unit: "Stk" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------- 20 Aufträge über alle Statusgruppen ----------
|
||||||
|
// Neu
|
||||||
|
await order({ ext: "DEMO-01", customer: "K-D004", site: "Praxisräume 2. OG", title: "Angebot Waschtisch-Armaturen prüfen", orderTypeId: type("besichtigung"), description: "Besichtigung der Behandlungsräume, Aufnahme der Armaturen.", status: "draft", plannedStart: day(6, 14), plannedEnd: day(6, 16) }, async () => undefined);
|
||||||
|
await order({ ext: "DEMO-02", customer: "K-D003", site: "Quartier Poppenbüttel – Block 5", title: "Strangsanierung Trinkwasser – Aufmaß", orderTypeId: type("montage"), description: "Aus Auftragsbestätigung übernommen, Prüfung erforderlich.", status: "review_required", plannedStart: day(10, 7), plannedEnd: day(14, 16), materials: heating.slice(1) }, async () => undefined);
|
||||||
|
// Geplant / kommend
|
||||||
|
await order({ ext: "DEMO-03", customer: "K-D006", site: "Verwaltungsgebäude", title: "Filterwechsel Lüftungsanlage", orderTypeId: type("wartung"), status: "planned", plannedStart: day(1, 8), plannedEnd: day(1, 12), materials: service }, async () => undefined);
|
||||||
|
await order({ ext: "DEMO-10", customer: "K-D005", site: "Einfamilienhaus Lindenallee", title: "Heizkörper Wohnzimmer tauschen", orderTypeId: type("montage"), priority: "normal", plannedStart: day(2, 8), plannedEnd: day(2, 15), materials: [{ name: "Flachheizkörper 22/600/1200", articleNumber: "HK-22-12", plannedQuantity: 1, unit: "Stk" }] }, (id) => assign(id, sued, [users.tech3]).then(() => undefined));
|
||||||
|
await order({ ext: "DEMO-11", customer: "K-D007", site: "Kita-Gebäude Hallerstraße", title: "Jahreswartung Gasheizung", orderTypeId: type("wartung"), plannedStart: day(5, 9), plannedEnd: day(5, 13), materials: service, technicianNotes: "Arbeiten nur in der Mittagsruhe (12–14 Uhr) mit Lärm." }, (id) => assign(id, nord, [users.tech]).then(() => undefined));
|
||||||
|
// Heute
|
||||||
|
await order({ ext: "DEMO-04", customer: "K-D002", site: "Backstube Osterstraße", title: "Wasserenthärtung warten", orderTypeId: type("wartung"), plannedStart: day(0, 13), plannedEnd: day(0, 15), materials: service }, (id) => assign(id, nord, [users.tech]).then(() => undefined));
|
||||||
|
await order({ ext: "DEMO-05", customer: "K-D001", site: "Wohnanlage Elbblick – Haus B", title: "Zirkulationspumpe tauschen", orderTypeId: type("reparatur"), priority: "high", plannedStart: day(0, 10), plannedEnd: day(0, 12), materials: [{ name: "Zirkulationspumpe 25-40", articleNumber: "ZP-2540", plannedQuantity: 1, unit: "Stk" }] }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech2]);
|
||||||
|
await transitionWorkOrder(tech2, { workOrderId: id, to: "accepted" });
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-06", customer: "K-D006", site: "Halle 1 – Kühllager", title: "Störung Kältemittelleitung", orderTypeId: type("stoerung"), priority: "urgent", plannedStart: day(0, 9), plannedEnd: day(0, 13) }, async (id) => {
|
||||||
|
await assign(id, sued, [users.tech3]);
|
||||||
|
await travel(tech3, id, day(0, 8, 30));
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-07", customer: "K-D003", site: "Quartier Poppenbüttel – Block 3", title: "Montage Gas-Brennwerttherme", orderTypeId: type("montage"), plannedStart: day(0, 7), plannedEnd: day(0, 17), materials: heating, scope: "Demontage Altgerät, Montage Neugerät, Abgasanschluss, Inbetriebnahme." }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech]);
|
||||||
|
await travel(tech, id, day(0, 6, 45));
|
||||||
|
await work(tech, id, day(0, 7, 10));
|
||||||
|
await photo(tech, id, { phase: "before", requirementKey: "ausgangszustand", comment: "Altgerät vor Demontage" });
|
||||||
|
await note(tech, id, "work_done", "Altgerät demontiert, Anschlüsse verschlossen.");
|
||||||
|
await note(tech, id, "deviation", "Abgasrohr muss um 30 cm verlängert werden.");
|
||||||
|
await planMaterial(tech, id, 1, 14, "fully_used", "Leitungsführung um den Schacht");
|
||||||
|
await material(tech, id, { clientId: randomUUID(), name: "Abgasrohr-Verlängerung DN80", articleNumber: "AG-80-05", quantity: 1, unit: "Stk", usageStatus: "additional", deviationReason: "Abgasrohr zu kurz" });
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-08", customer: "K-D002", site: "Filiale Eppendorf", title: "Undichte Leitung unter der Theke", orderTypeId: type("reparatur"), plannedStart: day(0, 7), plannedEnd: day(0, 11) }, async (id) => {
|
||||||
|
await assign(id, sued, [users.tech3]);
|
||||||
|
await work(tech3, id, day(0, 7, 5));
|
||||||
|
await note(tech3, id, "problem", "Leitung hinter Kühlvitrine schwer erreichbar.");
|
||||||
|
await pause(tech3, id, day(0, 9, 30));
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-09", customer: "K-D007", site: "Gartenhaus / Außenanlage", title: "Außenwasserhahn frostsicher erneuern", orderTypeId: type("reparatur"), plannedStart: day(0, 8), plannedEnd: day(0, 12), materials: [{ name: "Frostsichere Außenarmatur", articleNumber: "FA-12", plannedQuantity: 1, unit: "Stk" }] }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech2]);
|
||||||
|
await work(tech2, id, day(0, 8, 15));
|
||||||
|
await note(tech2, id, "problem", "Falsche Armatur geliefert (Länge 150 statt 250 mm).");
|
||||||
|
await transitionWorkOrder(tech2, { workOrderId: id, to: "waiting_material" });
|
||||||
|
await pause(tech2, id, day(0, 9));
|
||||||
|
});
|
||||||
|
// Überfällig
|
||||||
|
await order({ ext: "DEMO-12", customer: "K-D001", site: "Wohnanlage Elbblick – Haus B", title: "Thermostatventile Treppenhaus", orderTypeId: type("reparatur"), plannedStart: day(-4, 8), plannedEnd: day(-3, 16) }, (id) => assign(id, nord, [users.tech]).then(() => undefined));
|
||||||
|
await order({ ext: "DEMO-13", customer: "K-D006", site: "Verwaltungsgebäude", title: "Druckminderer erneuern", orderTypeId: type("reparatur"), plannedStart: day(-3, 8), plannedEnd: day(-2, 12) }, async (id) => {
|
||||||
|
await assign(id, sued, [users.tech3]);
|
||||||
|
await transitionWorkOrder(tech3, { workOrderId: id, to: "accepted" });
|
||||||
|
});
|
||||||
|
// Mehrtägig mit Tagesbericht
|
||||||
|
await order({ ext: "DEMO-14", customer: "K-D003", site: "Quartier Poppenbüttel – Block 5", title: "Heizungsverteilung Tiefgarage erneuern (2 Tage)", orderTypeId: type("montage"), plannedStart: day(-1, 7), plannedEnd: day(0, 16), materials: heating.slice(1), applyTemplate: false, checklistItems: [{ label: "Anlage entleert", required: true }, { label: "Druckprobe durchgeführt", required: true }] }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech, users.tech2]);
|
||||||
|
await work(tech, id, day(-1, 7));
|
||||||
|
await note(tech, id, "work_done", "Tag 1: Alte Verteilung demontiert, neue Konsolen gesetzt.");
|
||||||
|
await planMaterial(tech, id, 0, 8, "partially_used", "Rest wird an Tag 2 verbaut");
|
||||||
|
await photo(tech, id, { phase: "during", comment: "Neue Konsolen", takenAt: day(-1, 14) });
|
||||||
|
await end(tech, id, day(-1, 15, 30));
|
||||||
|
const { report } = await createDailyReport(tech, { workOrderId: id, reportDate: localDateKey(day(-1), TZ) });
|
||||||
|
await updateReportTexts(tech, { reportId: report.id, texts: { workPerformed: "Alte Verteilung demontiert, neue Konsolen gesetzt, Rohrleitungen vorbereitet.", openItems: "Anschluss der Heizkreise an Tag 2." } });
|
||||||
|
await submitReport(tech, { reportId: report.id });
|
||||||
|
await approveReport(lead, { reportId: report.id }, noPdf);
|
||||||
|
});
|
||||||
|
// Dokumentation unvollständig / Zur Prüfung
|
||||||
|
await order({ ext: "DEMO-15", customer: "K-D004", site: "Praxisräume 2. OG", title: "Boiler Sterilisationsraum tauschen", orderTypeId: type("montage"), plannedStart: day(-2, 13), plannedEnd: day(-2, 17), applyTemplate: false, checklistItems: [{ label: "Funktionsprüfung durchgeführt", required: true }], materials: [{ name: "Untertischspeicher 10 l", articleNumber: "UTS-10", plannedQuantity: 1, unit: "Stk" }] }, async (id) => {
|
||||||
|
await assign(id, sued, [users.tech3]);
|
||||||
|
await work(tech3, id, day(-2, 13, 10));
|
||||||
|
await checkAll(tech3, id);
|
||||||
|
await planMaterial(tech3, id, 0, 1, "fully_used");
|
||||||
|
await photo(tech3, id, { phase: "after", comment: "Neuer Speicher montiert", takenAt: day(-2, 16) });
|
||||||
|
await note(tech3, id, "work_done", "Speicher getauscht, Funktion geprüft.");
|
||||||
|
await end(tech3, id, day(-2, 16, 30));
|
||||||
|
await complete(tech3, id, { reportDate: day(-2), workPerformed: "Untertischspeicher demontiert und neu montiert, Dichtheit und Funktion geprüft.", outcome: "later", approve: "none", teamLead: lead2 });
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-16", customer: "K-D002", site: "Backstube Osterstraße", title: "Sicherheitsventil Warmwasserspeicher", orderTypeId: type("reparatur"), plannedStart: day(-1, 5), plannedEnd: day(-1, 8), applyTemplate: false, checklistItems: [{ label: "Anlage drucklos gemacht", required: true }] }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech2]);
|
||||||
|
await work(tech2, id, day(-1, 5, 10));
|
||||||
|
await checkAll(tech2, id);
|
||||||
|
await note(tech2, id, "work_done", "Sicherheitsventil getauscht und Ansprechdruck geprüft.");
|
||||||
|
await material(tech2, id, { clientId: randomUUID(), name: "Sicherheitsventil 6 bar", articleNumber: "SV-6", quantity: 1, unit: "Stk", usageStatus: "additional", deviationReason: "Nicht in der Vorgabe enthalten" });
|
||||||
|
await end(tech2, id, day(-1, 7, 20));
|
||||||
|
await complete(tech2, id, { reportDate: day(-1), workPerformed: "Sicherheitsventil am Warmwasserspeicher getauscht, Funktion geprüft.", outcome: "customer_absent", approve: "team", teamLead: lead });
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-17", customer: "K-D007", site: "Kita-Gebäude Hallerstraße", title: "Spülkasten Kinder-WC reparieren", orderTypeId: type("reparatur"), plannedStart: day(-1, 12), plannedEnd: day(-1, 14), applyTemplate: false, checklistItems: [{ label: "Spülung geprüft", required: true }] }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech]);
|
||||||
|
await work(tech, id, day(-1, 12, 5));
|
||||||
|
await checkAll(tech, id);
|
||||||
|
await note(tech, id, "work_done", "Füllventil und Dichtung ersetzt.");
|
||||||
|
await end(tech, id, day(-1, 13, 10));
|
||||||
|
await transitionWorkOrder(tech, { workOrderId: id, to: "technically_completed" });
|
||||||
|
});
|
||||||
|
// Zur Abrechnung / abgerechnet – beide mit freigegebenem Bericht in der Objekt-Historie von Haus A
|
||||||
|
const historySite = "Wohnanlage Elbblick – Haus A";
|
||||||
|
await order({ ext: "DEMO-18", customer: "K-D001", site: historySite, title: "Jahreswartung Gas-Brennwertkessel", orderTypeId: type("wartung"), plannedStart: day(-8, 8), plannedEnd: day(-8, 12), applyTemplate: false, checklistItems: [{ label: "Brenner gereinigt", required: true }, { label: "Abgaswerte gemessen", required: true }], materials: service }, async (id) => {
|
||||||
|
await assign(id, nord, [users.tech]);
|
||||||
|
await work(tech, id, day(-8, 8, 5));
|
||||||
|
await checkAll(tech, id);
|
||||||
|
await planMaterial(tech, id, 0, 1, "fully_used");
|
||||||
|
await planMaterial(tech, id, 1, 1, "partially_used", "Zweite Dichtung war noch in Ordnung");
|
||||||
|
await photo(tech, id, { phase: "after", comment: "Kessel nach Wartung", takenAt: day(-8, 11) });
|
||||||
|
await note(tech, id, "work_done", "Brenner gereinigt, Zündelektrode eingestellt, Abgaswerte im Sollbereich.");
|
||||||
|
await note(tech, id, "recommendation", "Ausdehnungsgefäß beim nächsten Termin prüfen.");
|
||||||
|
await end(tech, id, day(-8, 11, 30));
|
||||||
|
await complete(tech, id, { reportDate: day(-8), workPerformed: "Jahreswartung durchgeführt: Brenner gereinigt, Zündelektrode eingestellt, Abgasmessung ohne Befund.", outcome: "signed", approve: "final", teamLead: lead });
|
||||||
|
await releaseForBilling(bo, { workOrderId: id });
|
||||||
|
});
|
||||||
|
await order({ ext: "DEMO-19", customer: "K-D001", site: historySite, title: "Heizungspumpe erneuern", orderTypeId: type("reparatur"), plannedStart: day(-21, 9), plannedEnd: day(-21, 13), applyTemplate: false, checklistItems: [{ label: "Anlage entlüftet", required: true }], materials: [{ name: "Hocheffizienzpumpe 25-60", articleNumber: "HP-2560", plannedQuantity: 1, unit: "Stk" }] }, async (id) => {
|
||||||
|
await assign(id, sued, [users.tech3]);
|
||||||
|
await work(tech3, id, day(-21, 9, 10));
|
||||||
|
await checkAll(tech3, id);
|
||||||
|
await planMaterial(tech3, id, 0, 1, "fully_used");
|
||||||
|
await photo(tech3, id, { phase: "after", comment: "Neue Pumpe eingebaut", takenAt: day(-21, 12) });
|
||||||
|
await note(tech3, id, "work_done", "Heizungspumpe getauscht, Anlage entlüftet und hydraulisch abgeglichen.");
|
||||||
|
await note(tech3, id, "follow_up", "Thermostatköpfe im 3. OG klemmen – separaten Auftrag anlegen.");
|
||||||
|
await end(tech3, id, day(-21, 12, 40));
|
||||||
|
await complete(tech3, id, { reportDate: day(-21), workPerformed: "Heizungspumpe durch Hocheffizienzpumpe ersetzt, Anlage entlüftet.", outcome: "signed", signer: "Frauke Albers", approve: "final", teamLead: lead2 });
|
||||||
|
await releaseForBilling(bo, { workOrderId: id });
|
||||||
|
await markBilled(bo, { workOrderId: id });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notdienst mit vorläufigem Kunden (feste Client-IDs → idempotent)
|
||||||
|
const emergencyExisting = await bo.db.workSession.findFirst({ where: { clientId: "7c1d6a0e-3b1f-4c55-9d2a-00000000d020" }, select: { workOrderId: true } });
|
||||||
|
if (!emergencyExisting) {
|
||||||
|
const r = await createEmergencyOrder(tech, {
|
||||||
|
clientIds: { workOrder: "7c1d6a0e-3b1f-4c55-9d2a-00000000a020", session: "7c1d6a0e-3b1f-4c55-9d2a-00000000d020", customer: "7c1d6a0e-3b1f-4c55-9d2a-00000000c020", site: "7c1d6a0e-3b1f-4c55-9d2a-00000000e020" },
|
||||||
|
customer: { mode: "new", firstName: "Jonas", lastName: "Beispielmann", phone: "040 555 0299", street: "Heußweg", houseNumber: "71", postalCode: "20255", city: "Hamburg" },
|
||||||
|
site: { mode: "new", name: "Mehrfamilienhaus Heußweg 71", street: "Heußweg", houseNumber: "71", postalCode: "20255", city: "Hamburg" },
|
||||||
|
onSiteContact: { name: "Jonas Beispielmann", phone: "040 555 0299" },
|
||||||
|
reason: "Wasserrohrbruch im Keller – Hauptabsperrung defekt",
|
||||||
|
startedAt: day(0, 6, 20).toISOString(),
|
||||||
|
deviceInfo: "Demo-Seed",
|
||||||
|
});
|
||||||
|
await note(tech, r.workOrderId, "work_done", "Wasser abgesperrt, Leck provisorisch abgedichtet.");
|
||||||
|
await material(tech, r.workOrderId, { clientId: randomUUID(), name: "Reparaturschelle 1/2 Zoll", quantity: 1, unit: "Stk", usageStatus: "additional", deviationReason: "Notdienst" });
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = await bo.db.workOrder.count({ where: { OR: [{ externalOrderNumber: { startsWith: "DEMO-" } }, { isEmergency: true, emergencyReason: { startsWith: "Wasserrohrbruch im Keller" } }] } });
|
||||||
|
return { orders, created };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Isolation fixture for the second tenant: one customer + one work order. */
|
||||||
|
export async function seedDemo2Tenant(tenantId: string, adminUserId: string): Promise<void> {
|
||||||
|
const admin = ctxOf(tenantId, adminUserId, "tenant-admin");
|
||||||
|
let customer = await admin.db.customer.findFirst({ where: { customerNumber: "K-N001" }, select: { id: true } });
|
||||||
|
if (!customer) {
|
||||||
|
customer = await createCustomer(admin, { customerNumber: "K-N001", companyName: "Werft Nordlicht GmbH", street: "Am Hafen", houseNumber: "3", postalCode: "24103", city: "Kiel", phone: "0431 555 0101", notes: "Demo-Datensatz (Isolationsprüfung)" }, { acknowledgeDuplicates: true });
|
||||||
|
}
|
||||||
|
if (!(await admin.db.workOrder.findFirst({ where: { externalOrderNumber: "DEMO2-01" }, select: { id: true } }))) {
|
||||||
|
const site = await createSite(admin, { customerId: customer.id, name: "Schaltanlage Halle 2", street: "Am Hafen", houseNumber: "3", postalCode: "24103", city: "Kiel", accessNotes: "Anmeldung beim Werkschutz." });
|
||||||
|
await createWorkOrder(admin, { title: "Prüfung Unterverteilung nach DGUV V3", customerId: customer.id, siteId: site.id, externalOrderNumber: "DEMO2-01", status: "planned", plannedStart: day(3, 8), plannedEnd: day(3, 12) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close the owner client used by the services (the seed has its own client). */
|
||||||
|
export async function closeDemoSeed(): Promise<void> {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user