Files
craftvia/docs/craftvia/lanes/notdienst.md
T
2026-09-14 17:22:26 +02:00

16 KiB
Raw Blame History

Lane L8 – Notdienst

Branch lane/notdienst (Basis d5c1221 auf feature/craftvia-mvp, Welle 1 L1–L6 integriert). Spec §19 komplett, §39, US-010, §7.3, ARCHITEKTUR §3/§4.1/§4.6. Keine Schemaänderung, keine neue Migration, keine neuen npm-Abhängigkeiten.

1. Umfang / erfüllte Spec-Punkte

Spec Umsetzung
§19.1 / US-010 Monteur legt Notdienst selbst an /m/emergency (Modul emergency, Recht emergency:create + field:execute), jederzeit anlegbar (keine Geschäftszeitenprüfung, §19 Punkt 5 optional nicht umgesetzt)
§19.2 Erfassungsmaske 3 Schritte mit Schrittanzeige: 1 Kunde – Suche bestehender Kunden (Name/Ort/Kundennummer, Treffer zeigen nur Name · Nummer · Ort · „Vorläufig") oder „Neuer Kunde" (Firma/Name, Telefon Pflicht, E-Mail, Adresse); 2 Einsatzort – Objekt des Kunden oder Einsatzadresse (Straße + Ort Pflicht, „Kundenadresse übernehmen"), Ansprechpartner vor Ort + Telefon Pflicht; 3 Grund (Pflicht, Textfeld) + Sprachnotiz optional (lokal aufgenommen, nach dem Start hochgeladen und als voice.attach angehängt), Beginn (Default jetzt), Team (Default eigenes Team) und weitere Monteure des Teams (eigener User immer dabei). Große Felder (≥ 48 px), type="tel"/inputMode="tel" für Nummern, Status/Fehler immer mit Text + Icon.
§19.3 Vorläufige Datensätze createEmergencyOrder in einer Transaktion (inTransaction): vorläufiger Kunde (status provisional, isProvisional true, ohne Kundennummer) bzw. bestehender Kunde → Ansprechpartner (wiederverwendet bei gleichem Namen + Telefon) → vorläufiges Objekt (provisional) bzw. bestehendes Objekt des Kunden → Auftrag über L2 createWorkOrder (isEmergency, Auftragsart notdienst, Nummer aus Sequenz emergency N-…, Priorität urgent, Status in_progress, plannedStart = Beginn) → Team/Teamleiter/Zuweisungen → L4 startSession (WorkSession + Arbeitszeit-Segment). Danach Event emergency.created (startedAt). Anschließend normale Einsatzseite /m/orders/[id] (Zeiten, Material, Fotos, Tätigkeiten, Bericht, Unterschrift aus L4/L5).
Offline (ARCHITEKTUR §4.6) Sync-Op emergency.create mit clientIds { workOrder, session, customer?, site? }: Zod-Schema in src/lib/sync/ops.ts, Registry-Eintrag in services/sync/external-ops.ts. Idempotent doppelt: gleiche clientOpId → duplicate (L4), neue clientOpId mit gleicher Session-Client-ID → Replay liefert denselben Auftrag. Modul emergency wird in der Op geprüft (Sync-Route ist field-gegated). idMap bildet alle Client-IDs auf Server-IDs ab. Der Wizard sendet über submitOp (L7 kann die Outbox dahinter tauschen; die Client-IDs bleiben über Wiederholungen stabil).
§19.4 Benachrichtigung Nach Einreichen des Abschlussberichts (v1) eines Notdienstauftrags → emergency.completed mit number, technician (Ersteller), customer, startedAt (erste Session), endedAt (letztes Session-Ende), status (i. d. R. in_review), occurrenceId. L6 versendet die Pflichtmail im Format §19.4 und In-App ans Backoffice.
§19.3 / §39 Backoffice-Nachbearbeitung /work-orders/emergency-review (Recht emergency:review, Modul-Gate emergency + work_orders): Tabs „Zu prüfen" (vorläufiger Kunde/Objekt oder Status technically_completed/signature_pending/in_review) und „Alle Notdienste"; Karten mit Nummer, Status, Kunde/Objekt (Kennzeichnung „Vorläufig"), Monteur, Beginn, Fortschrittsbalken „x von 5 Prüfschritten erledigt". Detail /work-orders/emergency-review/[id] mit den Prüfschritten: 1 Kunde – bestätigen (Kundennummer wird jetzt vergeben, dann L1 confirmProvisionalCustomer) · bestehendem Kunden zuordnen (Kandidaten über L1 findDuplicateCustomers oder Kundennummer; Objekte, Ansprechpartner, Aufträge (Version +1), Dokumente werden umgehängt, vorläufiger Datensatz → merged + mergedIntoId) · Dublette zusammenführen (L1 mergeCustomers, customer:merge + Bestätigung); 2 Objekt – korrigieren/bestätigen (L1 updateSite) · bestehendem Objekt zuordnen (L2 updateWorkOrder, ungenutztes vorläufiges Objekt → Soft Delete über L1 deleteSite); 3 Auftrag ergänzen – Titel, Beschreibung, Auftragsart, Abrechnungsart (L2 updateWorkOrder, Versionsprüfung); 4 Bericht – Berichtsliste mit Link zur L5-Freigabe; 5 Abrechnung – L2 releaseForBilling (freigegebener Abschlussbericht), zusätzlich gesperrt solange Kunde/Objekt vorläufig (blocked, master_data_open).
Dashboard Kachel „neue Notdiensteinsätze" verlinkt für Nutzer mit emergency:review auf /work-orders/emergency-review (sonst weiter auf die L2-Liste).
§7.3 Dubletten Kandidaten in der Prüfung, niemals automatische Zusammenführung; Merge nur mit Bestätigung.

Jede Mutation: Permission (Guard in der Action und Service) · Scope (workOrderScope, bei der Erfassung Team-/Zuweisungsprüfung) · Zod · writeAuditLog (before/after; zusätzlich Entität emergency je Prüfschritt) · Events über emitEvent. Fachdaten ausschließlich über ctx.db, Mehrschritt-Schreibvorgänge über inTransaction.

Such-Service searchCustomersForEmergency(ctx, q): nur emergency:create, ab 2 Zeichen, max. 10 Treffer, nur aktive/vorläufige Kunden, Rückgabe { id, customerNumber, name, city, provisional }; jeder Zugriff wird auditiert (action "export", Entität emergency_customer_search, Suchbegriff + Treffer-IDs). listSitesForEmergency analog ({ id, name, address }, Entität emergency_site_lookup). Mandantentrennung über dbForTenant/RLS.

Sichtbarkeit: Ein vorläufiger Kunde ist für Monteure ausschließlich über einen sichtbaren Auftrag erreichbar – der bestehende workOrderScope enthält isEmergency && createdById = userId (ARCHITEKTUR §2), zusätzlich sehen Mitglieder/Teamleiter des gewählten Teams und Zugewiesene den Auftrag.

2. Dateien

Neu (Ownership L8)

  • src/lib/emergency/schemas.ts – Zod emergencyCreatePayload (Sync-Payload = Service-Input), emergencyOrderPatchSchema, REVIEW_STEPS
  • src/server/services/emergency/create.ts – createEmergencyOrder(ctx, input, deps?)
  • src/server/services/emergency/lookup.ts – searchCustomersForEmergency, listSitesForEmergency, emergencyTeamOptions
  • src/server/services/emergency/completion.ts – buildEmergencyCompletedData, onCompletionReportSubmitted
  • src/server/services/emergency/sync-ops.ts – applySyncOp für emergency.create
  • src/server/services/emergency/review.ts – listEmergencyReviews, getEmergencyReview, reviewProgress, confirmEmergencyCustomer, assignEmergencyToCustomer, mergeEmergencyCustomer, correctEmergencySite, assignEmergencySite, completeEmergencyOrderData, releaseEmergencyForBilling
  • src/server/actions/emergency/capture.ts (Suche/Objekte, moduleGuard("emergency") + guard("emergency:create")), src/server/actions/emergency/review.ts (7 Actions, je guard("emergency:review", …))
  • src/app/(field)/m/emergency/page.tsx (Platzhalter ersetzt), src/components/emergency/emergency-wizard.tsx
  • src/app/(app)/work-orders/emergency-review/{layout.tsx,page.tsx,[id]/page.tsx}, src/components/emergency/{review-form,review-progress}.tsx
  • messages/de/emergency.json, messages/en/emergency.json
  • scripts/test-notdienst-flow.ts, scripts/test-notdienst-review.ts

Fremd-Eingriffe (Einzeiler, im Auftrag erlaubt bzw. zwingend)

  • src/server/services/sync/external-ops.ts: Registry-Zeile emergency.create aktiviert
  • src/lib/sync/ops.ts: "emergency.create": emergencyCreatePayload (+ Import)
  • src/app/(app)/dashboard/page.tsx (L2): href der Kachel emergency_new → /work-orders/emergency-review (bei emergency:review)
  • src/lib/nav.ts: Eintrag „Notdienst-Prüfung" (module: "emergency", emergency:review, Icon Siren) + Label emergencyReview in messages/{de,en}/nav.json
  • src/components/audit-trail.tsx: Entity-Labels emergency_customer_search, emergency_site_lookup
  • src/server/services/reports/submit.ts (L5) – nicht in der Einzeiler-Liste, aber für Liefergegenstand 3 zwingend: Import + ein Aufruf await onCompletionReportSubmitted(ctx, wo.id, occurrenceId) direkt nach advanceOrder für Abschlussbericht v1. Der Hook ist ein No-op für normale Aufträge und wirft nie. Alternative ohne L5-Eingriff wäre nur ein Event-Abonnement im Fundament (emitEvent auf report.submitted) – bitte beim Merge bestätigen.

3. Tests

npm run gate grün: prisma generate, tsc (0 Fehler), lint (0 Fehler, 2 Warnungen in fremden Dateien), build inkl. Modul-Guard-Check (29 Action-Dateien), 44/44 Testskripte grün (davon 2 neu).

Skript Prüfungen Inhalt
test-notdienst-flow.ts 69 Erfassung mit vorläufigem Kunden (Nummer N-, in_progress + Historie, Auftragsart notdienst, Kunde/Objekt vorläufig ohne Kundennummer, Ansprechpartner, Default-Team/-Zuweisung, laufende WorkSession, Audit, emergency.created ans Backoffice, Akteur ausgenommen); Transaktion: Fehler nach dem Auftrags-Insert → kein Kunde/Objekt/Kontakt/Auftrag/Session, Nummernkreis ohne Lücke; Sichtbarkeit: vorläufiger Kunde für Ersteller sichtbar, anderer Monteur und Mandant B → not_found; Such-Service: fremde Kunden auffindbar, nur minimale Felder, Audit, Mandant B findet nichts, ohne emergency:create → forbidden, Objektabfrage minimal/auditiert/mandantengetrennt; bestehender Kunde + Objekt, Kontakt-Wiederverwendung, Objekt eines anderen Kunden/fremdes Team/fremder Kollege → invalid, Mandant B mit A-Kunden-ID → nichts angelegt, Backoffice → forbidden, Pflichtfelder; Sync-Op: applied mit idMap, gleiche clientOpId → duplicate, Replay mit neuer clientOpId → derselbe Auftrag ohne Doppelanlage, Offline-Flag, ungültige Payload → rejected invalid, Replay durch anderen Nutzer abgelehnt, Modul deaktiviert → forbidden; Abschluss: Session beenden → Abschlussbericht → Unterschrift „Kunde abwesend" → Absenden → Auftrag in_review, emergency.completed ans Backoffice, Daten Monteur/Kunde/Beginn ≤ Ende/Status, Pflichtmail protokolliert, normaler Auftrag ohne Notdienst-Event
test-notdienst-review.ts 49 Rollen: Monteur/Teamleiter → forbidden (Liste, Detail, bestätigen, zuordnen, Abrechnung, Merge); Mandant B: Liste leer, Detail/bestätigen/zuordnen/ergänzen → not_found, Daten unverändert; normaler Auftrag → not_found; Liste + Fortschritt 0/5 + Monteur; Dublettenkandidat über Telefon/Name; Beginn/Ende; Zuordnen: gleicher/fremder Kunde → invalid, Auftrag (Version +1), Objekt, Kontakt umgehängt, Quelle merged, Audit, erneut → conflict; Objekt: zuordnen + vorläufiges Objekt soft-gelöscht, fremdes Objekt → invalid, korrigieren + bestätigen; Bestätigen: K-Nummer vergeben, aktiv, zweites Mal → conflict; Merge ohne Bestätigung → invalid, Monteur → forbidden, mit Bestätigung umgehängt; Auftrag ergänzen inkl. Validierung und Versionskonflikt; Abrechnung: vorläufige Stammdaten → blocked master_data_open, ohne freigegebenen Bericht → blocked, nach Freigabe → released_for_billing, 5/5, aus „Zu prüfen" entfernt, unter „Alle" sichtbar, Prüfschritte auditiert

Hinweis Lane-DB: .env des Worktrees (nicht eingecheckt) nutzt craftvia_notdienst für DATABASE_URL und RLS_DATABASE_URL.

4. Stubs / Abhängigkeiten

  • Keine Stubs. Genutzt: L1 findDuplicateCustomers, confirmProvisionalCustomer, mergeCustomers, updateSite, deleteSite, customerScope; L2 createWorkOrder, updateWorkOrder, releaseForBilling, ensureDefaultOrderTypes, workOrderScope/activeTeamIds; L4 startSession, submitOp, uploadFieldFile, Sync-Registry; L5 Abschlussbericht/Unterschrift (unverändert) + Hook in submit.ts; L6 Empfänger/Pflichtmail für emergency.*.
  • L7 Offline: Der Wizard ruft submitOp({ opType: "emergency.create", … }). Liefert die Outbox später ein Ergebnis ohne Server-ID, zeigt der Wizard „wird übertragen, sobald Verbindung besteht". Die Kundensuche braucht Verbindung; offline bleibt „Neuer Kunde". Die Sprachnotiz wird nur online nach dem Start hochgeladen.

5. Bekannte Lücken / Hinweise

  1. L5-Eingriff (siehe §2): eine Zeile + Import in services/reports/submit.ts.
  2. Audit „Lesezugriff": AuditAction kennt kein read; der Suchzugriff wird als export protokolliert (Fundament-Vorschlag: Aktion read/access).
  3. mergeCustomers (L1) nutzt ctx.db.$transaction([...]) direkt und kann daher nicht innerhalb von inTransaction laufen; der Merge-Schritt ruft ihn eigenständig auf (L1 ist in sich atomar, bei RLS_ENFORCED=true gilt der Fundament-Hinweis aus §4.8).
  4. writeAuditLog schreibt über den Owner-Client außerhalb der Transaktion; eigene Audit-Einträge der Erfassung werden deshalb erst nach dem Commit geschrieben. createWorkOrder/startSession protokollieren innerhalb – bei einem Rollback bleiben deren Audit-Zeilen stehen (bekannt aus L2).
  5. assignEmergencySite läuft in inTransaction; updateWorkOrder emittiert sein Event dabei im Transaktionskontext (pg meldet eine Deprecation-Warnung wegen paralleler Queries im Transaktions-Client, funktional ohne Auswirkung).
  6. Globale Eindeutigkeit von WorkSession.clientId (Schema): ein Replay mit derselben Session-Client-ID in einem anderen Mandanten scheitert mit einem internen Fehler (nicht gespeichert, keine Datenwirkung).
  7. Geschäftszeiten-Hinweis (optional) nicht umgesetzt.
  8. PII_REFERENCE_FIELDS (Fundament): keine neuen Felder – genutzt werden bestehende (Customer.createdById, WorkOrder.createdById, WorkSession.userId).

6. Screens / Routen

Route Rolle Inhalt
/m/emergency Monteur, Teamleiter (emergency:create) 3-Schritt-Erfassung → „Einsatz starten" → /m/orders/[id]
/work-orders/emergency-review (?filter=all) Backoffice, Admin (emergency:review) Liste mit Fortschritt
/work-orders/emergency-review/[id] Backoffice, Admin Prüfschritte 1–5
POST /api/v1/sync Op emergency.create Monteur, Teamleiter Offline-/Online-Anlage

7. Smoke

HTTP-Smoke gegen den Dev-Server :3108 (Lane-DB, Seed-Mandanten demo/demo2). Session-Cookies wurden lokal ohne Passworteingabe erzeugt (finalizeIdentityLogin + Auth.js encode). 15/16 Prüfungen grün:

  • ohne Session: /m/emergency → 307 Login
  • Monteur: /m/emergency rendert Schritt 1 (bestehender/neuer Kunde); Backoffice sieht dort den Hinweis „nicht freigeschaltet"
  • POST /api/v1/sync emergency.create → applied mit idMap; gleiche clientOpId → duplicate; fremder Origin → 403; Backoffice (ohne emergency:create) → abgelehnt
  • Monteur: /m/orders/[id] des neuen Notdienstes rendert (N-Nummer, Grund)
  • Backoffice: /work-orders/emergency-review (Fortschritt), ?filter=all, Detail mit allen Prüfschritten → 200
  • Mandant demo2: Detail → 404, Liste ohne Notdienste von demo
  • Monteur/Teamleiter: Prüfseiten → 307 (nicht zugänglich)
  • Navigation „Notdienst-Prüfung" wird gerendert
  • ✗ /dashboard → 500 im Dev-Server (vorbestehender L2-Fehler, nicht durch L8): dashboard/page.tsx ruft buttonCls() aus der Client-Datei components/work-orders/action-form.tsx („use client") in einer Server-Komponente auf (Zeilen 134/137, schon auf d5c1221 vorhanden). Die neue Kachel-Verlinkung ist deshalb nur im Code geprüft, nicht im gerenderten Dashboard. Fix-Vorschlag an L2/Architekt: buttonCls in eine Datei ohne „use client" verschieben.

Nicht durchgeführt: visuelle Prüfung im Browser (Responsive 375/768/1024 px) und Aufrufe der Review-Server-Actions über die UI (fachlich durch test-notdienst-review.ts abgedeckt). Der Smoke hinterlässt zwei Smoke-Notdienste im Mandanten demo der Lane-DB.