16 KiB
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– ZodemergencyCreatePayload(Sync-Payload = Service-Input),emergencyOrderPatchSchema,REVIEW_STEPSsrc/server/services/emergency/create.ts–createEmergencyOrder(ctx, input, deps?)src/server/services/emergency/lookup.ts–searchCustomersForEmergency,listSitesForEmergency,emergencyTeamOptionssrc/server/services/emergency/completion.ts–buildEmergencyCompletedData,onCompletionReportSubmittedsrc/server/services/emergency/sync-ops.ts–applySyncOpfüremergency.createsrc/server/services/emergency/review.ts–listEmergencyReviews,getEmergencyReview,reviewProgress,confirmEmergencyCustomer,assignEmergencyToCustomer,mergeEmergencyCustomer,correctEmergencySite,assignEmergencySite,completeEmergencyOrderData,releaseEmergencyForBillingsrc/server/actions/emergency/capture.ts(Suche/Objekte,moduleGuard("emergency")+guard("emergency:create")),src/server/actions/emergency/review.ts(7 Actions, jeguard("emergency:review", …))src/app/(field)/m/emergency/page.tsx(Platzhalter ersetzt),src/components/emergency/emergency-wizard.tsxsrc/app/(app)/work-orders/emergency-review/{layout.tsx,page.tsx,[id]/page.tsx},src/components/emergency/{review-form,review-progress}.tsxmessages/de/emergency.json,messages/en/emergency.jsonscripts/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-Zeileemergency.createaktiviertsrc/lib/sync/ops.ts:"emergency.create": emergencyCreatePayload(+ Import)src/app/(app)/dashboard/page.tsx(L2):hrefder Kachelemergency_new→/work-orders/emergency-review(beiemergency:review)src/lib/nav.ts: Eintrag „Notdienst-Prüfung" (module: "emergency",emergency:review, IconSiren) + LabelemergencyReviewinmessages/{de,en}/nav.jsonsrc/components/audit-trail.tsx: Entity-Labelsemergency_customer_search,emergency_site_lookupsrc/server/services/reports/submit.ts(L5) – nicht in der Einzeiler-Liste, aber für Liefergegenstand 3 zwingend: Import + ein Aufrufawait onCompletionReportSubmitted(ctx, wo.id, occurrenceId)direkt nachadvanceOrderfü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 (emitEventaufreport.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; L2createWorkOrder,updateWorkOrder,releaseForBilling,ensureDefaultOrderTypes,workOrderScope/activeTeamIds; L4startSession,submitOp,uploadFieldFile, Sync-Registry; L5 Abschlussbericht/Unterschrift (unverändert) + Hook insubmit.ts; L6 Empfänger/Pflichtmail füremergency.*. - 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
- L5-Eingriff (siehe §2): eine Zeile + Import in
services/reports/submit.ts. - Audit „Lesezugriff":
AuditActionkennt keinread; der Suchzugriff wird alsexportprotokolliert (Fundament-Vorschlag: Aktionread/access). mergeCustomers(L1) nutztctx.db.$transaction([...])direkt und kann daher nicht innerhalb voninTransactionlaufen; der Merge-Schritt ruft ihn eigenständig auf (L1 ist in sich atomar, beiRLS_ENFORCED=truegilt der Fundament-Hinweis aus §4.8).writeAuditLogschreibt über den Owner-Client außerhalb der Transaktion; eigene Audit-Einträge der Erfassung werden deshalb erst nach dem Commit geschrieben.createWorkOrder/startSessionprotokollieren innerhalb – bei einem Rollback bleiben deren Audit-Zeilen stehen (bekannt aus L2).assignEmergencySiteläuft ininTransaction;updateWorkOrderemittiert sein Event dabei im Transaktionskontext (pg meldet eine Deprecation-Warnung wegen paralleler Queries im Transaktions-Client, funktional ohne Auswirkung).- 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). - Geschäftszeiten-Hinweis (optional) nicht umgesetzt.
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/emergencyrendert Schritt 1 (bestehender/neuer Kunde); Backoffice sieht dort den Hinweis „nicht freigeschaltet" POST /api/v1/syncemergency.create→appliedmit idMap; gleicheclientOpId→duplicate; fremder Origin → 403; Backoffice (ohneemergency: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 vondemo - 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.tsxruftbuttonCls()aus der Client-Dateicomponents/work-orders/action-form.tsx(„use client") in einer Server-Komponente auf (Zeilen 134/137, schon aufd5c1221vorhanden). Die neue Kachel-Verlinkung ist deshalb nur im Code geprüft, nicht im gerenderten Dashboard. Fix-Vorschlag an L2/Architekt:buttonClsin 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.