docs/craftvia/lanes/einsatz.md: Umfang, Routen, Dateien, Tests, Stubs und Abhängigkeiten, bekannte Lücken, Gate- und Smoke-Ergebnis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
Lane L4 – Einsatz mobil (lane/einsatz)
Stand: 2026-09-14 · Basis bf44567 (feature/craftvia-mvp)
1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §11.2 Auftragsansicht | /m Heute (heute geplant + laufend + pausiert), /m/orders Tabs Kommend · Laufend · Abzuschließen · Vergangen; große Karten mit Nummer, Kunde, Objektadresse + Routenlink, Zeitfenster, Statusgruppe (Text + Icon + Statuskante), Primärbutton |
| §12.1 Einsatzstart | session.start speichert Startzeit, Benutzer, Team, Auftrag, optional Standort, Gerätestatus (User-Agent), Offline-Flag; optional „Losfahren“ (Anfahrt) vor Arbeitsbeginn |
| §12.2 Arbeitszeiten | Session aus TimeEntry-Segmenten (Anfahrt → Arbeit ⇄ Pause), Start/Pause/Weiter/Ende, Dauerberechnung; Korrektur nur mit field:correct_time, Grund Pflicht, corrected-Flag, Audit before/after |
| §12.3 Tätigkeitsdokumentation | Notizen mit Art (9 große Chips) + Text; Entwurf bleibt bei Verbindungsabbruch lokal erhalten (US-006); Sprachnotiz |
| §12.4 Checklisten | Abhaken mit großem Schalter + Kommentar |
| §13.2/§13.3 Material | Planposition vollständig/teilweise/nicht verwendet mit Stepper-Menge; Begründung bei Abweichung Pflicht (UI + Server, gemeinsame Regeln); Zusatzmaterial mit Bezeichnung, Artikelnummer, Menge, Einheiten-Vorschlägen, Grund |
| §14.1/§14.3/§14.4 Fotos | Kamera (capture="environment") + Galerie; Phase Vorher/Während/Nachher, Pflichtfoto (Kategorie), Checklistenpunkt, Kommentar, optional Standort; Kompression im Browser (max. 2560 px, JPEG 0.82, Vorschaubild 400 px, EXIF-Orientierung über imageOrientation: "from-image"), Upload-Fortschritt; Metadaten: Auftrag, Einsatz, Benutzer, Zeit, MIME (Magic Bytes), Größe, Prüfsumme, Uploadstatus; serverseitiges Vorschaubild per Job, falls der Client keines liefert |
| §15.1 Sprachnotizen | MediaRecorder (webm/opus, mp4 auf iOS), max. 5 min, Upload → VoiceNote pending → dispatchJob("transcription"); ohne registrierten Processor (L9) Status disabled |
| §22 Mobile Ansicht | eigene Shell ohne Sidebar, Bottom-Nav Heute · Aufträge · Notdienst · Sync · Profil, Online/Offline-Badge, Touch-Ziele ≥ 48 px, eine Primäraktion je Zustand, Kamera in 2 Taps (Karte → Foto) |
| US-005 | Dokumente am Auftrag + Objekt (nur erlaubte Sichtbarkeit, backoffice_only verborgen), freigegebene Berichte der Objekt-Historie; Auslieferung über autorisierte Route |
| US-006 | Einsatz starten/beenden, Fotos, Material bestätigen/ändern, Zusatzmaterial, Notizen mit lokalem Entwurf |
| ARCHITEKTUR §4.6 (Server) | src/lib/sync/ops.ts, applyOperations, POST /api/v1/sync, POST /api/v1/uploads, GET /api/v1/field/bundle?since= |
Primäraktion je Zustand (components/field/primary-action.tsx): assigned → Annehmen · accepted → Losfahren (sekundär: Arbeit starten) · en_route → Arbeit starten · in Arbeit mit laufender eigener Session → Abschließen (sekundär Pause) · pausiert → Weiter · technically_completed/signature_pending → Hinweis „Bericht und Unterschrift folgen“. Abschließen beendet die eigene Session und ruft work_order.transition → technically_completed; offene Pflichtpunkte werden vorher als Liste angezeigt und der Button gesperrt.
2. Routen / Screens
| Route | Inhalt |
|---|---|
/m |
Heute |
/m/orders?tab= |
Auftragsliste mit Tabs |
/m/orders/[id] |
Kopf (Status, Zeitfenster, Primäraktion), Schnellaktionen (Foto direkt · Notiz · Material · Checkliste · Zeiten), Hinweise für Monteure, Objekt (Zugang/Parken/Sicherheit/Technik hervorgehoben, Route), Kunde/Ansprechpartner (tel:/mailto:), Auftrag, Dokumente, Objekt-Historie, Zusammenfassungen |
/m/orders/[id]/photos · /notes · /materials · /checklist · /time |
Unterseiten (max. 2 Ebenen unter der Liste) |
/m/profile |
Name, Betrieb, Team, Rolle, Sprache, Abmelden (Link ins Büro für Teamleiter/Backoffice) |
/m/sync |
Platzhalter für L7 (Verbindungsstatus, „Änderungen werden sofort übertragen“) |
/m/emergency |
unverändert Platzhalter von L8 (nur verschoben) |
POST /api/v1/sync |
Batch-Ops → SyncResponse |
POST /api/v1/uploads |
multipart file, clientId, workOrderId, kind (photo/voice_note), optional preview → { documentId, duplicate } |
GET /api/v1/field/bundle?since= |
Offline-Pull |
GET /api/v1/field/documents/[id]?variant=preview |
autorisierte Dokument-Auslieferung (Sichtbarkeit + Scope) |
Route-Group-Umzug: src/app/(app)/m/** → src/app/(field)/m/**. (field)/m/layout.tsx = Shell (Zugriffsprüfung), (field)/m/(core)/layout.tsx = Modul-Gate field, emergency/layout.tsx = Modul-Gate emergency (unverändert). Die Zugriffsprüfungen aus (app)/layout.tsx sind nach src/server/app-access.ts#requireAppAccess extrahiert und werden von beiden Shells genutzt (erlaubter Fundament-Eingriff, ARCHITEKTUR §5).
Header-Slot für L6: im Mobile-Header ((field)/m/layout.tsx) ist links neben dem Online-Badge Platz für <NotificationBell variant="mobile" /> markiert.
3. Dateien
Neu (Ownership L4)
src/app/(field)/m/layout.tsx,(core)/layout.tsx(verschoben),(core)/page.tsx,(core)/orders/page.tsx,(core)/orders/[id]/{page.tsx,load.ts},(core)/orders/[id]/{photos,notes,materials,checklist,time}/page.tsx,(core)/profile/page.tsx,sync/page.tsx(Platzhalter L7)src/app/api/v1/sync/route.ts,src/app/api/v1/uploads/route.ts,src/app/api/v1/field/bundle/route.ts,src/app/api/v1/field/documents/[id]/route.tssrc/server/services/field/:common.ts,sessions.ts,time-correction.ts,checklist.ts,materials.ts,notes.ts,photos.ts,voice.ts,uploads.ts,documents.ts,queries.ts,page-context.ts,stubs/{work-order-transition,documents-store,site-history}.tssrc/server/services/sync/apply.ts,src/server/services/sync/api-context.ts,src/server/services/sync/external-ops.ts(Registry für Ops anderer Lanes)src/server/actions/field/time.ts(Zeitkorrektur,moduleGuard("field"))src/server/jobs/processors/image-derivatives.tssrc/lib/sync/ops.ts,src/lib/field/{client-ops,upload,image,format,material-rules}.tssrc/components/field/*(Shell-Navigation, Online-Badge, Statusbadge, Auftragskarte, Primäraktion, Foto, Sprachnotiz, Notiz, Material, Stepper, Checkliste, Zeitkorrektur, UI-Klassen)messages/de/field.json,messages/en/field.jsonscripts/test-einsatz-field.ts,scripts/test-einsatz-sync.ts,scripts/lib/einsatz-fixture.ts
Fundament / Fremd-Einzeiler
src/server/app-access.ts(neu, extrahiert) +src/components/account-inactive-notice.tsx(Hinweis „Konto deaktiviert“ aus dem Layout herausgelöst, Text unverändert) +src/app/(app)/layout.tsxnutzt beidessrc/app/page.tsx: rollenabhängige Startseite (landingPath:field:executeohnework_order:read_all→/m, sonst/dashboard)src/app/login/page.tsx,src/app/login/mfa/page.tsx: Default-Redirect nach Login/dashboard→/(je 1 Zeile, + „bereits angemeldet“-Redirect)src/server/jobs/processors/index.ts: Registrierungimage-derivatives(1 Zeile)src/components/audit-trail.tsx: Entity-Labels fürwork_session,time_entry,checklist_item,material_usage,photo,voice_note,activity_note,sync_operation(1 Zeile)
Keine Schemaänderung, keine neue Migration, keine neuen npm-Abhängigkeiten (sharp ist über Next vorhanden).
4. Tests
scripts/test-einsatz-field.ts– Session-Zustände (keine doppelte laufende Session, Pause-Segmente, Ende berechnet Dauer), Abschluss-Guards, Zeitkorrektur (Recht, Grund, Ende ≥ Beginn, Audit before/after), Material (Abweichung ohne Grund → invalid, Zusatzmaterial, Idempotenz), Checkliste/Notiz, Scope (Monteur ohne Zuweisung → not_found; ohnefield:execute→ forbidden), Sichtbarkeit/Bundle, Mandantentrennung.scripts/test-einsatz-sync.ts– Idempotenz (gleiche clientOpId → duplicate, keine Doppelanlage, je Mandant), Konflikt bei veralteter baseVersion (nichts überschrieben, SyncOperationconflict), Scope über Sync (fremder Auftrag → not_found, kein Versions-Leak), Ops fremder Lanes (rejected invalid, nicht gespeichert), Uploads (Idempotenz, Magic Bytes, Mandantentrennung beim Hochladen/Öffnen/Anhängen,backoffice_onlyverborgen), Foto/Sprachnotiz (Statusdisabled), image-derivatives-Processor inkl. Fremdmandant.
Ergebnis: siehe Abschnitt 7 (Gate).
5. Stubs & Abhängigkeiten zu anderen Lanes
| Stub (in L4-Pfad) | Vertrag | Ersetzen durch |
|---|---|---|
services/field/stubs/work-order-transition.ts |
transitionWorkOrder(ctx, { workOrderId, to, reason?, baseVersion? }) → { id, from, status, version }; completionBlockers(ctx, workOrderId) → CompletionBlocker[]; wirft ServiceError inkl. blocked mit Blockern |
L2 services/work-orders/transition.ts (Importe in sessions.ts, queries.ts, sync/apply.ts, Test) |
services/field/stubs/documents-store.ts |
storeFile(ctx, { bytes, fileName, declaredMime, category, visibility, links, lineageId? }) → Document (§4.3) inkl. Magic-Byte-Prüfung/Limits/SHA-256 |
gemeinsames services/documents/store.ts (Import in uploads.ts) |
services/field/stubs/site-history.ts |
getSiteHistory(ctx, siteId, { onlyApproved, limit? }) → SiteHistoryEntry[] |
L1 (Importe in queries.ts) |
app/(field)/m/sync/page.tsx |
Platzhalter | L7 ersetzt die Seite und lib/field/client-ops.ts#submitOp (Signatur stabil) |
Sync-Ops report.save_draft, report.submit, signature.capture |
Lazy-Import-Registry services/sync/external-ops.ts (auskommentierte Einzeiler) → services/reports/sync-ops.ts#applySyncOp(ctx, op) → { idMap?, entityVersion? } |
L5 liefert das Modul und aktiviert je Op eine Zeile in external-ops.ts; bis dahin rejected invalid („not available yet“), nicht gespeichert |
Sync-Op emergency.create |
Registry-Einzeiler in services/sync/external-ops.ts → services/emergency/sync-ops.ts#applySyncOp |
L8 |
| Transkription | Processor transcription in jobs/processors/index.ts |
L9; bis dahin VoiceNote disabled |
/m/orders/[id]/{report,sign} |
noch nicht angelegt | Komponenten von L5, Einbindung durch L4 nach Merge |
6. Bekannte Lücken / Hinweise an den Architekten
-
Registry statt berechnetem
import(): Ein Import mit berechnetem Pfad (import(../${dir}/sync-ops)) kann Turbopack nicht auflösen (Build-Warnung, Modul wäre nach Merge nicht ladbar). Daher explizite Registryservices/sync/external-ops.ts– L5/L8 brauchen dort je Op einen Einzeiler. -
Offline: Ops werden sofort gesendet; ohne Verbindung wird nichts zwischengespeichert (außer Notiz-Entwurf). Outbox/Service Worker = L7.
-
Bundle-Löschungen:
bundle?since=liefert nur geänderte/offene Aufträge, keine Tombstones für entzogene/abgeschlossene Aufträge (Client muss Vollabgleich ohnesincemachen). -
Jobs (Fundament-Bug):
enqueueJoberzeugtjobIdmit:– BullMQ lehnt das ab („Custom Id cannot contain :“),dispatchJobfällt deshalb immer auf Inline-Ausführung zurück. Fix insrc/server/jobs/queues.ts(z. B.-statt:) nötig. -
requireApiContextwird inservices/context.tserwähnt, existiert im Fundament aber nicht → lane-lokal inservices/sync/api-context.ts(nutztmoduleGuard, Same-Origin-Prüfung, Fehler-Mapping). Kandidat für das Fundament. -
DSGVO
pii-fields.ts(Fundament): Personenreferenzen der Einsatzmodelle sind noch nicht eingetragen:WorkSession.userId,TimeEntry.userId,TimeEntry.correctedById,ActivityNote.authorId,Photo.takenById,VoiceNote.recordedById,MaterialUsage.recordedById,ChecklistItem.checkedById,Document.uploadedById,SyncOperation.userId/resolvedById. -
/files/[...key]prüft weiterhin nur das Mandantenpräfix (TODO documents); die Mobile-App nutzt deshalb/api/v1/field/documents/[id]mit Sichtbarkeits- und Scope-Prüfung. -
Transaktionen: Session-Operationen (Segment schließen/öffnen + Statuswechsel) laufen sequenziell ohne DB-Transaktion; bei parallelen Doppelstarts desselben Users ist eine zweite aktive Session theoretisch möglich (kein Unique-Index). Idempotenz über
clientId/clientOpIddeckt Wiederholungen ab. -
„Heute“ wird in der Server-Zeitzone berechnet, Anzeige in
Europe/Berlin(lib/field/format.ts); Mandanten-Zeitzone fehlt im Modell. -
Passkey-Login und Mandantenwechsel (
tenant-switch.ts,/select-tenant) leiten weiter fest auf/dashboard– Feldrollen landen dort erst über die Startseite/, nicht direkt auf/m. -
HEIC ohne Browser-Dekodierung: Upload scheitert mit Hinweis (keine serverseitige Konvertierung).
7. Gate & Smoke
Tests: test-einsatz-field.ts 48 Prüfungen, test-einsatz-sync.ts 38 Prüfungen – alle grün (S3/Garage-Pfad aktiv; ohne S3_ENDPOINT werden nur die Byte-Abrufe übersprungen).
Gate: npm run gate grün – prisma generate, tsc, lint (0 Fehler, 2 Warnungen im Fundament-Platzhalter handle-event.ts), build inkl. Modul-Guard-Check (14 Action-Dateien), 24/24 Testskripte.
HTTP-Smoke (Dev-Server :3104, Demo-Mandant mit Smoke-Auftrag, Session-Cookies für monteur@, multi@ (Monteur ohne Zuweisung) und admin2@ (Mandant demo2)): 32/32 Prüfungen grün – Startseite / → /m (Monteur) bzw. /dashboard (Admin), alle Mobile-Seiten 200 mit erwarteten Inhalten, fremder/zugriffsloser Auftrag → 404, Backoffice-Dashboard rendert weiterhin, Sync (not_found für fremd, applied+duplicate, conflict bei veralteter Version, 400 bei leerem Batch, 403 bei Cross-Origin, ohne Session → Login-Redirect), Bundle, Upload 201/200 (idempotent)/404 (fremder Mandant), Dokument-Original + Vorschau 200 bzw. 404 für fremd, photo.attach applied.
Mobile-UX 375×812 (Browser-Pane): große Karten mit Statuskante, Status als Text + Icon, eine orange Primäraktion, Schnellaktionen mit Kamera direkt (Heute → Karte → Foto = 2 Taps), Bottom-Nav 64 px, Buttons ≥ 48 px, max. 2 Ebenen unter der Liste; Klickpfad Annehmen → Losfahren über die UI geprüft.