// 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 { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import sharp from "sharp"; import { FakeExtractionProvider } from "../../src/server/ai/extraction/fake"; import { createImport } from "../../src/server/services/imports/upload"; import { processImport } from "../../src/server/services/imports/process"; import { buildPdf } from "../make-sample-pdfs"; 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(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 { const svg = `${label}`; return sharp(Buffer.from(svg)).jpeg({ quality: 80 }).toBuffer(); } async function signaturePng(): Promise { const 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[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(); 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(); 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) => 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 & { ext: string; customer: string; site?: string }; const order = async (seed: OrderSeed, drive: (id: string) => Promise) => { 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" }); }); // DEMO-06/DEMO-07 (laufende Uhren von Sven Südfeld und Max Monteur) werden erst nach DEMO-19 angelegt: seit // L12 darf ein Nutzer nur eine laufende Uhr haben – spätere Einsätze derselben Monteure würden sonst kollidieren. 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 }); }); // Heute laufende Uhren (nach allen vergangenen Einsätzen derselben Monteure, s. o.) 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" }); }); // 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++; } // Import in Prüfung (Prüfmaske): Musterdokument + Extraktion über den Fake-Provider (keine KI nötig) if (!(await bo.db.importJob.findFirst({ where: { extractedText: { startsWith: "DEMO-IMPORT" } }, select: { id: true } }))) { const samplePath = join(process.cwd(), "docs", "craftvia", "samples", "02-elbblick-wartungsauftrag.pdf"); const bytes = existsSync(samplePath) ? readFileSync(samplePath) : buildPdf([[{ text: "Wartungsauftrag Hausverwaltung Elbblick GmbH" }]]); const job = await createImport(bo, { bytes, fileName: "wartungsauftrag-elbblick.pdf", mimeType: "application/pdf" }, { dispatch: async () => undefined }); await processImport(bo, job.id, { provider: new FakeExtractionProvider({ text: "DEMO-IMPORT Wartungsauftrag Hausverwaltung Elbblick GmbH, Große Elbstraße 140, 22767 Hamburg", extraction: { orderNumber: { value: "WA-2026-117", confidence: 0.96 }, customerNumber: { value: "K-D001", confidence: 0.91 }, companyName: { value: "Hausverwaltung Elbblick GmbH", confidence: 0.95 }, customerAddress: { value: { street: "Große Elbstr.", houseNumber: "140", postalCode: "22767", city: "Hamburg", country: "DE" }, confidence: 0.9 }, siteName: { value: "Wohnanlage Elbblick – Haus B", confidence: 0.72, source: "Objekt: Haus B" }, siteAddress: { value: { street: "Große Elbstraße", houseNumber: "140b", postalCode: "22767", city: "Hamburg" }, confidence: 0.88 }, contactName: { value: "Frauke Albers", confidence: 0.83 }, title: { value: "Wartung Warmwasserbereitung", confidence: 0.86 }, plannedStart: { value: localDateKey(day(12), TZ), confidence: 0.64, source: "KW-Angabe unsicher" }, positions: { value: [{ name: "Wartungsset Speicher", articleNumber: "WS-S2", quantity: 1, unit: "Satz", isMaterial: true }, { name: "Arbeitszeit", quantity: 2, unit: "Std", isMaterial: false }], confidence: 0.8 }, }, }), loadBytes: async () => bytes, }); } 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 { 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 { await prisma.$disconnect(); }