Files
craftvia/docs/craftvia/lanes/einsatz.md
T
msolarczekandClaude Opus 5 edded26b21 L4 Einsatz mobil: Lane-Bericht
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>
2026-09-14 12:30:03 +02:00

14 KiB
Raw Blame History

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.ts
  • src/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}.ts
  • src/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.ts
  • src/lib/sync/ops.ts, src/lib/field/{client-ops,upload,image,format,material-rules}.ts
  • src/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.json
  • scripts/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.tsx nutzt beides
  • src/app/page.tsx: rollenabhängige Startseite (landingPath: field:execute ohne work_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: Registrierung image-derivatives (1 Zeile)
  • src/components/audit-trail.tsx: Entity-Labels für work_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; ohne field:execute → forbidden), Sichtbarkeit/Bundle, Mandantentrennung.
  • scripts/test-einsatz-sync.ts – Idempotenz (gleiche clientOpId → duplicate, keine Doppelanlage, je Mandant), Konflikt bei veralteter baseVersion (nichts überschrieben, SyncOperation conflict), 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_only verborgen), Foto/Sprachnotiz (Status disabled), 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

  1. 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 Registry services/sync/external-ops.ts – L5/L8 brauchen dort je Op einen Einzeiler.

  2. Offline: Ops werden sofort gesendet; ohne Verbindung wird nichts zwischengespeichert (außer Notiz-Entwurf). Outbox/Service Worker = L7.

  3. Bundle-Löschungen: bundle?since= liefert nur geänderte/offene Aufträge, keine Tombstones für entzogene/abgeschlossene Aufträge (Client muss Vollabgleich ohne since machen).

  4. Jobs (Fundament-Bug): enqueueJob erzeugt jobId mit : – BullMQ lehnt das ab („Custom Id cannot contain :“), dispatchJob fällt deshalb immer auf Inline-Ausführung zurück. Fix in src/server/jobs/queues.ts (z. B. - statt :) nötig.

  5. requireApiContext wird in services/context.ts erwähnt, existiert im Fundament aber nicht → lane-lokal in services/sync/api-context.ts (nutzt moduleGuard, Same-Origin-Prüfung, Fehler-Mapping). Kandidat für das Fundament.

  6. 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.

  7. /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.

  8. 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/clientOpId deckt Wiederholungen ab.

  9. „Heute“ wird in der Server-Zeitzone berechnet, Anzeige in Europe/Berlin (lib/field/format.ts); Mandanten-Zeitzone fehlt im Modell.

  10. 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.

  11. 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.