Merge lane/berichte in feature/craftvia-mvp

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:23:34 +02:00
co-authored by Claude Opus 5
57 changed files with 4781 additions and 6 deletions
+80
View File
@@ -0,0 +1,80 @@
# Lane L5 – Berichte & Unterschrift
Branch `lane/berichte` (Basis `bf44567`, `feature/craftvia-mvp`). Spec §16, §17, §18, §32, US-007/008/009, ARCHITEKTUR §4.7.
## Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §16 Tagesbericht | `createDailyReport`: Entwurf je Auftrag + Kalendertag (Mandanten-Zeitzone), Inhalte nur des Tages (Zeiten, Notizen, Fotos, Material). Auftrag → `daily_report_created` über `transitionWorkOrder` (aus `paused`/`waiting_material` über `in_progress`), Auftrag bleibt offen. Idempotent je Tag. Event `work_order.daily_report_created`. |
| §17.1/17.2 Abschlussbericht | `createCompletionReport` prüft `getCompletionBlockers` (Checkliste, Pflichtfotos, laufende Zeiten) → `ServiceError("blocked", details: CompletionBlocker[])`. Snapshot mit allen Inhalten aus §17.2 (`ReportContent`). |
| Pflichtangaben | Absenden verlangt „Ausgeführte Leistungen“ (`REPORT_REQUIRED_TEXTS`) + erneut Blocker-Prüfung → strukturierte Liste. |
| §17.3 PDF | Nach Freigabe (`report:approve`) Job `report-pdf` → `generateReportPdf`: HTML-Template (React SSR) → Chromium/Playwright → Dokument (Kategorie `daily_report`/`completion_report`, Sichtbarkeit `customer_report`, an Auftrag/Kunde/Objekt → erscheint in Objekt-Historie), `pdfDocumentId` + SHA-256 `pdfChecksum`. Bestehendes PDF wird nie ersetzt. |
| §17.4 Versionierung | `createNewVersion` (nur `report:approve`): neue Version gleiche `lineageId`, `version+1`, Entwurf aus freigegebenem Snapshot. Die freigegebene Version bleibt unverändert und wird erst bei Freigabe der Nachfolgerin `superseded` (so existiert immer ein gültiges freigegebenes PDF). |
| §18.1 Unterschrift | `captureSignature`: Name, Funktion, Datum/Uhrzeit (`signedAt`), Bestätigungstext (messages, mit Berichts-/Auftragsnummer + Datum), Bezug Bericht, erfassender Nutzer. Canvas-Pad mit Pointer Events (Maus/Touch/Stift), Löschen, PNG-Export → Server Action → Dokument `signature`. |
| §18.2 Ausnahmen | `customer_absent`/`refused`/`later` → Begründung Pflicht; `not_required` nur bei `signatureRequired=false` oder `report:approve`. Erfasste Unterschrift (`signed`) wird nie überschrieben; `later` kann nachgereicht werden (Auftrag `signature_pending` → `in_review`). |
| §32 PDF-Merkmale | Bericht-ID, Versionsnummer, Erstellungs-/Freigabedatum, Freigabestatus, Prüfsumme (Fuß: SHA-256 des Inhalts-Snapshots; SHA-256 der PDF-Datei am Bericht/Dokument), A4, Seitenumbrüche, Fotoraster 2-spaltig, Kopf-/Fußzeile mit Seitenzahl, Mandantenlogo sonst Firmenname, Inter eingebettet. |
| Freigabe | Teamleiter (`report:approve_team`) → `team_approved`; Backoffice (`report:approve`) → `approved` (Inhalt final eingefroren, Event `report.approved`, PDF-Job). Zurückweisen mit Pflichtgrund → `rejected`, Abschluss-Auftrag `in_review` → `in_progress`, Event `report.rejected`. |
| Auftragsstatus bei Absenden | Abschlussbericht v1: … → `in_progress` → `technically_completed` → `signature_pending` (keine Unterschrift/`later`) bzw. `in_review` (`signed`, `not_required`, `refused`, `customer_absent`; bei den letzten beiden zusätzlich Event `work_order.signature_missing`). Folgeversionen ändern den Auftragsstatus nicht. |
| US-009 Berichtsseite | `/reports` (zur Prüfung zuerst, Filter Typ/Status/Team/Zeitraum), `/reports/[id]` (strukturierte Ansicht, PDF-Link, Versionen, Aktionen, Badge „Lotse-Entwurf“ bei `aiDrafted`). |
## Dateien
- Vertrag/Client-safe: `src/lib/reports/content.ts` (Zod `ReportContent`, Status-/Outcome-Konstanten), `src/lib/reports/dates.ts` (Tagesfenster Zeitzone), `src/lib/reports/action-state.ts`
- Services `src/server/services/reports/`: `build-content.ts`, `common.ts` (Scope `reportScope`/`requireVisibleReport`, Audit, Refresh), `create.ts`, `edit.ts`, `submit.ts`, `approve.ts`, `reject.ts`, `new-version.ts`, `signature.ts`, `pdf.ts`, `files.ts` (Datei-Auslieferung nur für im Snapshot referenzierte Dokumente), `queries.ts` (Liste/Detail/Mobil), `read-ctx.ts`, `http.ts` (API-Adapter), `_stubs/{work-orders,documents}.ts`
- PDF: `src/server/pdf/render.ts`, `src/server/pdf/templates/report.tsx`, Processor `src/server/jobs/processors/report-pdf.ts`
- Actions `src/server/actions/reports/`: `workflow.ts` (create/save/submit/approve/reject/newVersion/regeneratePdf), `signature.ts`, `_state.ts`
- API: `POST /api/v1/work-orders/[id]/daily-report`, `POST /api/v1/work-orders/[id]/completion-report`, `POST /api/v1/reports/[id]/approve`, `GET /api/v1/reports/[id]/pdf`, zusätzlich `GET /api/v1/reports/[id]/files/[documentId]` (Fotos/Unterschrift in Ansicht)
- UI Backoffice: `src/app/(app)/reports/page.tsx`, `src/app/(app)/reports/[id]/page.tsx`; Komponenten `src/components/reports/{report-view,review-actions,reject-form,status-badge,blocker-list,action-message,step-indicator,signature-pad}.tsx`
- UI Mobil: `src/components/reports/mobile/{report-editor,report-review,sign-flow,report-screen,sign-screen,create-report-form}.tsx`; dünne Seiten `src/app/(app)/m/(field)/orders/[id]/{report,sign}/page.tsx`
- Texte: `messages/de/reports.json`, `messages/en/reports.json`
- Tests: `scripts/test-berichte-flow.ts`, `scripts/test-berichte-pdf.ts`
Fremd-Einzeiler/erlaubte Eingriffe: `src/server/jobs/processors/index.ts` (Registrierung `report-pdf`, mit `turbopackIgnore`), `Dockerfile` (neue Stage `worker` am Ende). `src/lib/nav.ts` war bereits eingetragen. `package.json`/`package-lock.json`: neue Abhängigkeit `playwright-core`.
## Abhängigkeit `playwright-core`
`playwright-core@^1.63` (Apache-2.0, ~8 MB entpackt, kein postinstall-Download, keine transitiven Laufzeit-Abhängigkeiten). Vom Architektur-Vertrag vorgegeben (§1 PDF). Wird nur im Worker geladen (`turbopackIgnore` im Processor-Registry-Eintrag), nicht im App-Bundle. Browser-Auflösung: `PDF_CHROMIUM_PATH` → Playwright-Chromium → lokal installiertes Google Chrome (`channel: "chrome"`, Entwicklerrechner).
**Worker-Image:** braucht Chromium + Schriften. Vorschlag als Stage `worker` im `Dockerfile` (Debian-Paket `chromium`, `fonts-dejavu-core`, `fonts-liberation`, `PDF_CHROMIUM_PATH=/usr/bin/chromium`, Start `npx tsx scripts/craftvia-worker.ts`). Compose-Service (`target: worker`, `REDIS_URL`, `DATABASE_URL`, `S3_*`) ist noch einzutragen (nicht Lane-Ownership).
## Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
| `scripts/test-berichte-flow.ts` | 70: Content-Builder (Tagesfilter Zeiten/Notizen/Fotos/Material, Materialabweichungen Menge/nicht verwendet/zusätzlich/undokumentiert, Kopfdaten), Tagesbericht + Auftragsstatus + Idempotenz + Nummernkreis, Abschluss-Blocker, Pflichtangaben, Unterschrift-Validierung je outcome, Statusfolgen submit/team_approve/reject/resubmit/approve inkl. Auftragsstatus, Einfrieren nach Freigabe, Versionierung (v1 nie überschrieben, superseded erst bei Freigabe v2), Mandantentrennung (lesen/ändern/freigeben/Liste/direkter DB-Update), Rollen/Scope (Monteur ohne Zuweisung, Teamleiter anderes Team, Monteur ohne Freigaberecht), Audit before/after | grün |
| `scripts/test-berichte-pdf.ts` | 11: PDF gerendert, Dokument > 0 Bytes, beginnt mit `%PDF`, Kategorie/Sichtbarkeit/Auftragsbezug, SHA-256 gespeichert und = Bytes, zweiter Lauf überschreibt nicht, Download eigener Mandant, Mandant B → not_found, nicht referenziertes Dokument → not_found. Ohne startbaren Browser: Skip mit Meldung (Exit 0) | grün (lokal über Google Chrome) |
Gate: siehe Rückmeldung an den Architekten (`npm run gate` grün).
## Stubs / Abhängigkeiten zu anderen Lanes
| Stub | Vertrag | Ersetzen durch |
|---|---|---|
| `services/reports/_stubs/work-orders.ts#transitionWorkOrder` | ARCHITEKTUR §3 (canTransition + requiredPermission, Scope, `WorkOrderStatusChange`, Version+1, Audit, Event) | L2 `services/work-orders/transition.ts` |
| `services/reports/_stubs/work-orders.ts#getCompletionBlockers` | §3 Guards vor Abschluss → `CompletionBlocker[]` | L2 (Datei gemäß L2, z. B. `services/work-orders/guards.ts`) |
| `services/reports/_stubs/documents.ts#storeFile/readFileBytes` | §4.3 `services/documents/store.ts` (Allowlist, Magic Bytes, Größenlimit, SHA-256, Lineage) | Architekt/Dokumente (`services/documents/store.ts`) |
| Unterschrift-Upload über Server Action (Data-URL) | §4.6 `POST /api/v1/uploads` | L4 – für Offline-Sync (`signature.capture` referenziert `documentId`) |
Imports sind mit `TODO(merge …)` markiert. `/api/v1/reports/...` nutzt einen eigenen Adapter (`http.ts`, `moduleGuard("reports")`), da noch kein gemeinsames `requireApiContext` existiert.
L4 bindet die mobilen Seiten ein: Link „Bericht“ im Auftragsdetail → `/m/orders/[id]/report` (Tabs Abschluss/Tag), Abschluss → `/m/orders/[id]/sign`. Sync-Ops `report.save_draft`/`report.submit`/`signature.capture` können direkt `updateReportTexts`/`submitReport` (`expectedWorkOrderVersion`)/`captureSignature` (`clientId`) aufrufen.
## Bekannte Lücken / offene Punkte
- **Schema unverändert** (keine Migration). Mandantenlogo: `TenantSettings.logoKey` ist kein `Document` → `logoDocumentId` bleibt `null`, PDF zeigt Firmennamen, bis ein Logo-Upload als Dokument existiert.
- Berichtsnummer (`B-…`) liegt im Snapshot (`content.reportNumber`), nicht als Spalte – Suche nach Nummer braucht ggf. eine Spalte (Architekt).
- PDF wird ohne laufenden Worker nicht erzeugt, solange `REDIS_URL` gesetzt ist (Job bleibt in der Queue). Ohne Redis läuft `dispatchJob` inline, der Processor ist im App-Bundle aber absichtlich nicht enthalten → Fehler wird geloggt, Freigabe bleibt gültig, „PDF erzeugen“ auf der Detailseite stößt den Job erneut an.
- `generateReportPdf` rendert Fotos in Originalgröße (Data-URI); bei vielen großen Fotos ggf. auf Derivate (L4 `image-derivatives`) umstellen.
- E-Mail-Versand des PDF (§17.3 „optional“) und Empfänger der Events liegen bei L6.
- Mobile-Seiten sind ohne L4-Shell nur über direkte URL erreichbar; Offline-Fähigkeit (L7) nicht Teil dieser Lane.
- `src/server/dsgvo/pii-fields.ts`: `Report.createdById/approvedById/teamApprovedById`, `Signature.capturedById`, `signerName` sollten vom Architekten eingetragen werden (Fundament-Datei, nicht geändert).
## Screens / Routen
| Route | Rolle | Inhalt |
|---|---|---|
| `/reports` | Backoffice, Teamleiter (Scope) | Liste, zur Prüfung zuerst, Filter Typ/Status/Team/Von–Bis |
| `/reports/[id]` | Backoffice, Teamleiter (Scope) | Strukturierte Ansicht, Metadaten, PDF-Link, Versionen, Freigeben/Als Teamleiter prüfen/Zurückweisen (Popup `?reject=1`)/Neue Version/PDF erzeugen |
| `/m/orders/[id]/report?type=completion\|daily` | Monteur, Teamleiter | Blocker → Bericht erstellen → prüfen/ergänzen → (Tag) absenden / (Abschluss) weiter zur Unterschrift |
| `/m/orders/[id]/sign` | Monteur, Teamleiter | Unterschrift (Pad) oder Grund, dann Bericht absenden |