L2 Aufträge & Backoffice: Lane-Bericht docs/craftvia/lanes/auftraege.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:30:54 +02:00
co-authored by Claude Opus 5
parent b49d14bab3
commit a9530f2513
+115
View File
@@ -0,0 +1,115 @@
# Lane L2 – Aufträge & Backoffice
Branch `lane/auftraege` (Basis `feature/craftvia-mvp` @ bf44567). Keine Schemaänderung, **keine neue Migration**, keine neuen npm-Abhängigkeiten.
## 1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §10.1 Auftragsdaten | Anlage/Bearbeitung aller Felder (Kunde, Objekt, Ansprechpartner, Auftragsart, Priorität, Zeitraum, Beschreibung, Leistungsumfang, Hinweise intern/Monteure, Unterschrift erforderlich, Abrechnungsart, externe Auftrags-/Angebotsnummer). Soft Delete nur für Entwürfe. |
| §10.2 Auftragsarten | `/settings/order-types`; `ensureDefaultOrderTypes` legt die 8 Standardarten beim ersten Zugriff je Mandant an (idempotent). |
| §10.3 Statusmodell / ARCHITEKTUR §3 | `transition.ts#transitionWorkOrder` ist der einzige Status-Schreibpfad: Übergangstabelle (`status.ts`), Rechte je Übergang, Scope, Begründungspflicht (Storno, Korrektur, Freigabe zurücknehmen), Guards → `ServiceError("blocked", …, CompletionBlocker[])`, optimistische Versionsprüfung (`baseVersion`), `WorkOrderStatusChange`, Audit, Event. |
| §10.4 / US-004 Teamzuweisung | `assign.ts#assignWorkOrder`: Team + optionale Einzelpersonen + Teamleiter (Default: Teamleiter des Teams), draft/review_required/planned → assigned, Teamwechsel nach „angenommen“ → assigned, Event `work_order.assigned`, Audit. Popup im Detail. |
| §12.4 Checklisten, §14.2 Pflichtfotos | Vorlagen je Auftragsart (`/settings/checklists`, Standardvorschlag aus §12.4/§14.2); Übernahme beim Anlegen; Pflege am Auftrag (Punkte/Pflichtfotos hinzufügen/entfernen, Vorlage nachträglich übernehmen). Erledigte Punkte / Pflichtfotos mit Fotos bleiben als Nachweis. |
| Completion-Guards | `completion.ts#getCompletionBlockers`: offene Pflichtpunkte (inkl. „mit Foto“ ohne Foto), Pflichtfotos ohne Foto, offene WorkSession. Zusätzlich: `→ in_review` nur mit erfasster Unterschrift (jede Outcome-Art, §18.2) wenn `signatureRequired`; `→ released_for_billing` nur mit freigegebenem Abschlussbericht. |
| §13.1 Materialvorgabe | CRUD (`materials.ts`), Tab „Material“ mit Soll/Ist inkl. Abweichung, Begründung und Zusatzmaterial. |
| §21 Dashboard | `/dashboard`: 11 Kacheln (offen, heute, laufend, nicht angenommen, überfällig, Berichte zur Prüfung, abgeschlossen, zur Abrechnung, neue Notdienste, fehlende Unterschriften, Sync-Konflikte), Filter Zeitraum/Kunde/Team/Monteur/Auftragsart/Priorität; jede Kachel verlinkt auf `/work-orders?preset=…` (bzw. Konfliktliste). Feldrollen (ohne `work_order:read_all`) → Redirect `/m`. „Heute“ rechnet in der Mandanten-Zeitzone. |
| §21 Liste | `/work-orders`: Statusgruppen-Tabs mit Zählern, Filterleiste (Zeitraum, Kunde, Objekt, Team, Monteur, Status, Auftragsart, Priorität, Freitext), Sortierung, Paginierung, Karten- (Statuskante) und Tabellenansicht, Anlage-Popup mit Kundensuche → Objekt/Kontakt-Auswahl. |
| Detail | `/work-orders/[id]`: Kopf mit Status (Text + Icon), nächster primärer Aktion (CTA), weiteren Übergängen, Bearbeiten/Zuweisen/Stornieren; Tabs Übersicht · Checkliste & Pflichtfotos · Material · Zeiten (Lesesicht) · Fotos (Galerie) · Notizen · Berichte (Liste + Abrechnungsaktionen) · Dokumente (Upload) · Verlauf (Statusänderungen + Audit). |
| US-009 Abrechnung | Freigabe, Zurückweisung zur Korrektur (mit Grund), Freigabe zurücknehmen, „abgerechnet“ – protokolliert, Event `work_order.released_for_billing`. |
| §23.5 Konflikte | `/work-orders/conflicts`: offene `SyncOperation(status=conflict)` mit Payload-Vorschau; „Übernehmen“ (Op erneut gegen aktuellen Stand als ursprünglicher Gerätenutzer) / „Verwerfen“ (resolvedAt/By + Audit). |
| §25 Suche | `/search?q=` + Header-Suchfeld: Aufträge (Nummer, externe/Angebotsnummer, Titel, Beschreibung, Leistungsumfang, Objektadresse), Kunden (inkl. Adresse), Objekte, Ansprechpartner, Dokumentnamen, Notizen; ILIKE mit `workOrderScope/customerScope/siteScope` und Dokument-Sichtbarkeit; Filter Zeitraum/Status/Team. |
| Nummernkreise | `/settings/numbering` (Präfix/Stellen; Zähler wird nie zurückgesetzt). |
| API | `GET/POST /api/v1/work-orders`, `GET/PATCH /api/v1/work-orders/[id]`, `POST …/assign`, `POST …/transition`, `GET/POST …/materials`, zusätzlich `POST …/documents` (multipart). Fehler: 401/403/404/409/422 (blocked mit `details: CompletionBlocker[]`). |
Jede Mutation: Permission (Guard + Service) · Scope (`workOrderScope`) · Zod · `writeAuditLog` (before/after) · `version + 1` · ggf. `emitEvent`. Fachdaten ausschließlich über `ctx.db`.
## 2. Verträge für andere Lanes
Exakte Signaturen (Stubs von L3/L5 darauf umstellen):
```ts
// src/server/services/work-orders/create.ts
createWorkOrder(ctx: ServiceCtx, raw: CreateWorkOrderInput): Promise<{ id: string; number: string; status: string; version: number }>
// CreateWorkOrderInput (src/lib/work-orders/schemas.ts): title, customerId, siteId?, contactId?, orderTypeId?, priority?,
// status?: "draft" | "review_required" | "planned" | "in_progress" (Default draft; in_progress nur mit isEmergency),
// description?, scope?, plannedStart?, plannedEnd?, signatureRequired?, billingType?, internalNotes?, technicianNotes?,
// externalOrderNumber?, offerNumber?, isEmergency?, emergencyReason?, sourceImportId?, numberKey?: "work_order" | "emergency",
// applyTemplate? (Default true), materials?, checklistItems?, photoRequirements?
// src/server/services/work-orders/transition.ts
transitionWorkOrder(ctx: ServiceCtx, raw: { workOrderId: string; to: WorkOrderStatus; reason?: string | null; baseVersion?: number;
eventData?: Record<string, string | number | boolean | null> }): Promise<{ id: string; status: WorkOrderStatus; version: number; from: WorkOrderStatus }>
// eventData wird in event.data gemischt; number/from/to werden nie überschrieben.
// src/server/services/work-orders/completion.ts
getCompletionBlockers(ctx: ServiceCtx, workOrderId: string): Promise<CompletionBlocker[]> // mit Scope-Prüfung (not_found)
computeCompletionBlockers(ctx: ServiceCtx, workOrderId: string): Promise<CompletionBlocker[]> // ohne Scope-Prüfung, für bereits geladene Aufträge
```
- **Transaktionen:** L2 verwendet **kein `$transaction`**. Die Services arbeiten ausschließlich über `ctx.db`. Ein `ctx`, dessen `db` ein Transaktions-Client ist (künftig `inTransaction(ctx, fn)`), wird unverändert unterstützt. Ausnahme: `writeAuditLog` schreibt über den Owner-Client (Fundament) außerhalb der Transaktion. `createWorkOrder` legt Auftrag, Checkliste, Pflichtfotos, Material und die erste Statushistorie in **einem** verschachtelten `create` an (atomar). Die Nummernvergabe (`nextNumber`) läuft davor. Nicht atomare Mehrschritt-Schreibvorgänge, die nach dem Fundament-Merge in `inTransaction` gehören: `applyTransition` (Versions-Update → StatusChange → Audit), `assignWorkOrder` (Auftrag → Assignees löschen/anlegen → StatusChange), Material-/Checklisten-Mutationen (Zeile → `touchWorkOrder`), `applySyncConflict` (Re-Apply → SyncOperation-Update).
- L3 Import: `status: "review_required"` oder `"planned"`, `sourceImportId`, optional `materials`, `checklistItems`, `photoRequirements`.
- L8 Notdienst: `isEmergency: true`, `numberKey: "emergency"` (N-…), `status: "in_progress"` erlaubt nur mit `isEmergency`; Recht `work_order:write` **oder** `emergency:create`.
- `transitionWorkOrder(ctx, { workOrderId, to, reason?, baseVersion? })` – für L4 (Sync `work_order.transition`) und L5 (Berichte).
- `getCompletionBlockers(ctx, id)`, `assignWorkOrder`, `cancelWorkOrder`, `releaseForBilling` / `rejectForCorrection` / `revokeBillingRelease` / `markBilled`.
- `ensureDefaultOrderTypes(db, tenantId)` – kann in `provision.ts` aufgerufen werden (Fundament, nicht angefasst).
## 3. Dateien
**Service/Lib (neu):** `src/lib/work-orders/{schemas,defaults,filters,time,action-state}.ts`; `src/server/services/work-orders/{_shared,create,update,transition,completion,assign,cancel,release-billing,materials,checklist,list,dashboard,detail,search,conflicts,settings,options,documents,sync-reapply}.ts`
**Actions:** `src/server/actions/work_orders/{_form,work-orders,settings}.ts`
**UI:** `src/app/(app)/work-orders/{page.tsx,[id]/page.tsx,conflicts/page.tsx}`, `src/app/(app)/dashboard/page.tsx`, `src/app/(app)/search/page.tsx`, `src/app/(app)/settings/{order-types,checklists,numbering}/page.tsx`, `src/components/work-orders/{action-form,ui,page-context,work-order-list,work-order-fields,detail-tabs,settings-nav}.tsx|ts`
**API:** `src/app/api/v1/work-orders/{_http.ts,route.ts,[id]/route.ts,[id]/assign,[id]/transition,[id]/materials,[id]/documents}`
**Texte:** `messages/{de,en}/{workOrders,dashboard,search,settingsTemplates}.json`
**Tests:** `scripts/test-auftraege-{status,core,scope,numbering}.ts`, Fixtures `scripts/lib-auftraege-fixtures.ts` (kein `test-`-Präfix → nicht vom Runner ausgeführt)
**Erlaubte Fremd-Einzeiler:**
- `src/lib/nav.ts`: Eintrag „Auftragsvorlagen“ → `/settings/order-types` (`settings:templates`) + Label `templates` in `messages/{de,en}/nav.json`
- `src/app/(app)/layout.tsx`: Header-Suchfeld → `GET /search`
- `src/components/audit-trail.tsx`: Entity-Labels `order_type`, `checklist_template`, `number_sequence`, `sync_operation`
## 4. Tests
`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler; 2 Warnungen im Fundament-Platzhalter `services/notifications/handle-event.ts`), build inkl. Modul-Guard-Check (15 Action-Dateien), **26/26 Testskripte grün**, davon 4 neue L2-Skripte (alle Nachweise erfüllt).
Hinweis Lane-DB: `scripts/test-rls-enforcement.ts` nutzt ohne `RLS_DATABASE_URL` die Standard-DB `craftvia`. In der lokalen `.env` des Worktrees (nicht eingecheckt) ist `RLS_DATABASE_URL` auf `craftvia_auftraege` gesetzt (Rolle `craftvia_app`, lokales Testpasswort laut Testskript).
| Skript | Inhalt |
|---|---|
| `test-auftraege-status.ts` | alle 256 Statuspaare gegen den Vertrag; **180 Rolle×Übergang-Kombinationen** (tenant-admin, backoffice, team-lead, technician) mit unabhängig aus der Rechte-Tabelle abgeleiteter Erwartung (Erfolg inkl. Version/Historie/Audit bzw. forbidden ohne Schreibwirkung); verbotene Paare → invalid; Begründungspflicht; Versionskonflikt |
| `test-auftraege-core.ts` | Anlage (Nummer, Vorlage/Unterschrift aus Auftragsart, Notdienst N-), Update (Version, Konflikt, Kunde/Objekt-Konsistenz, Audit before/after, Soft Delete), Zuweisung, Completion-Blocker (Checkliste/Pflichtfoto/laufende Session), Unterschrift vor in_review, Freigabe nur mit freigegebenem Abschlussbericht, Material-CRUD + Soll/Ist, Checklistenpflege, Liste (Presets, Filter, Gruppen, Paginierung), Dashboard-Kacheln, Konflikte verwerfen/übernehmen, Auftragsarten/Vorlagen/Nummernkreis |
| `test-auftraege-scope.ts` | Monteur fremdes Team → not_found (Liste/Detail/Übergang/Blocker); Team-Mitglied/Teamleiter/Einzelzuweisung sehen; Monteur ohne Backoffice-Rechte → forbidden; **Mandant B**: Liste/Detail/Update/Übergang/Zuweisung/Material/Konflikt/Dashboard/Suche ohne Zugriff auf A, Tenant-Guard direkt; Suche respektiert Scope und Filter |
| `test-auftraege-numbering.ts` | 20 parallele `createWorkOrder` auf frischem Mandanten → eindeutig und lückenlos A-00001…A-00020; zweiter Mandant eigener Kreis |
## 5. Stubs / Abhängigkeiten
| Stub (in L2-Pfad) | Ersetzt durch | Vorgehen nach Merge |
|---|---|---|
| `services/work-orders/documents.ts#uploadWorkOrderDocument` (Allowlist PDF/JPEG/PNG/WebP, Magic Bytes, Größenlimit, SHA-256, `storage.put`, `Document`) | L1 `services/documents/store.ts#storeFile` | Rumpf durch `storeFile(ctx, { …, links: { workOrderId, customerId, siteId } })` ersetzen |
| `services/work-orders/sync-reapply.ts#reapplySyncOperation` (nur `work_order.transition`) | L4 `services/sync/apply.ts` | an L4-Dispatcher delegieren; Signatur bleibt |
| Tab „Berichte“: einfache Liste mit Link `/reports/[id]` | L5-Komponente | optional L5-Komponente einbinden |
| Dateilinks `/files/<storageKey>` | Umstellung auf `/files/<documentId>` (ARCHITEKTUR §4.3, L1) | Links in `detail-tabs.tsx` anpassen |
Events werden über `emitEvent` gesendet (`work_order.assigned|changed|cancelled|started|daily_report_created|technically_completed|signature_missing|released_for_billing`); Empfängerauflösung liefert L6.
## 6. Bekannte Lücken / Hinweise an Fundament/Architekt
- **`requireApiContext` existiert nicht** (in `context.ts` referenziert). `/api/v1/work-orders` nutzt `moduleGuard("work_orders")` (Session-Cookie, DB-autoritative Rechte). Token-Auth für Mobile/Integrationen fehlt; `src/proxy.ts` leitet `/api/v1/**` ohne Cookie per 307 auf `/login` statt 401 → Fundament-Bedarf.
- Upload-Größe: Dokument-Upload läuft über Route-Handler (kein 1-MB-Server-Action-Limit), aber `proxyClientMaxBodySize` (Default 10 MB) begrenzt – für PDFs bis 25 MB in `next.config.ts` erhöhen (Fundament).
- Einstellungsseiten liegen ohne Modul-Layout unter `/settings/*`; die Actions laufen über `moduleGuard("work_orders")` (bei deaktiviertem Modul nicht speicherbar).
- `PII_REFERENCE_FIELDS` (`src/server/dsgvo/pii-fields.ts`, Fundament) enthält die Personen-Referenzen des Auftragsmodells noch nicht (`WorkOrder.createdById/teamLeadUserId`, `WorkOrderAssignee.userId`, `WorkOrderStatusChange.actorId`, `SyncOperation.resolvedById`) → Architekt/Fundament.
- Suche in Berichtsinhalten (JSON) nicht umgesetzt (Berichte gehören L5).
- Dashboard „Berichte zur Prüfung“ zählt Berichte (`submitted`/`team_approved`); die Kachel verlinkt auf Aufträge mit solchen Berichten.
- Bearbeiten-Popup ändert den Kunden nicht (API `PATCH` kann es, inkl. Konsistenzprüfung).
- Browser-Smoke mit Anmeldung nicht automatisiert (siehe §7).
## 7. Screens / Routen
`/dashboard` · `/work-orders` (`?group=`, `?preset=`, `?view=table`, `?new=1`) · `/work-orders/[id]` (`?tab=overview|checklist|material|times|photos|notes|reports|documents|history`, `?assign=1`, `?edit=1`, `?transition=<status>`) · `/work-orders/conflicts` · `/search?q=` · `/settings/order-types` · `/settings/checklists` · `/settings/numbering`
**Smoke (Dev-Server Port 3102, curl):** alle Seiten oben sowie `GET /api/v1/work-orders`, `GET /api/v1/work-orders/[id]` und `POST …/transition` antworten ohne Session mit 307 → `/login?callbackUrl=…` (Proxy-Gate greift, Server startet fehlerfrei). Alle Seiten und Route-Handler werden im Production-Build fehlerfrei kompiliert. Die i18n-Schlüssel sind statisch geprüft: 808 Verwendungen, alle in de und en vorhanden. **Nicht durchgeführt:** Smoke mit Anmeldung / visuelle Prüfung (Responsive 1024/768 px). Der Agent gibt keine Passwörter in Login-Abläufe ein; bitte manuell mit einem Seed-Nutzer prüfen (`backoffice@demo.example` → `/dashboard`, `/work-orders`; `monteur@demo.example` → Redirect `/m`).