L4 Einsatz mobil: Mobile Shell und Einsatz-Oberfläche

- /m in eigene Route-Group (field)/m verschoben (emergency-Platzhalter mit),
  Zugriffsprüfungen aus (app)/layout.tsx nach server/app-access.ts extrahiert
  und von Backoffice- und Mobile-Shell gemeinsam genutzt
- Mobile Shell mit Bottom-Nav (Heute · Aufträge · Notdienst · Sync · Profil)
  und Online/Offline-Badge; Startseite rollenabhängig (Feldrollen → /m),
  Login-Default-Redirect auf /
- Heute, Auftragsliste mit Tabs, Auftragsdetail mit einer Primäraktion je
  Zustand, Unterseiten Fotos (Kamera, Kompression, Upload-Fortschritt),
  Notizen + Sprachaufnahme, Material mit Stepper, Checkliste, Zeiten mit
  Korrektur, Profil; Sync-Platzhalter für L7
- Client-Wrapper submitOp (lib/field/client-ops.ts), Upload mit Fortschritt,
  Bildkompression, Formatierung; Texte in messages/{de,en}/field.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:30:03 +02:00
co-authored by Claude Opus 5
parent 952e74cddd
commit 95f41b29f5
42 changed files with 3186 additions and 66 deletions
+298
View File
@@ -0,0 +1,298 @@
{
"nav": {
"label": "Einsatz-Navigation",
"today": "Heute",
"orders": "Aufträge",
"emergency": "Notdienst",
"sync": "Sync",
"profile": "Profil"
},
"connection": {
"online": "Online",
"offline": "Offline"
},
"noAccess": "Die Einsatz-App ist für Ihre Rolle nicht freigeschaltet.",
"today": {
"title": "Heute",
"greeting": "Hallo {name}",
"count": "{count, plural, =0 {Keine Einsätze} one {# Einsatz} other {# Einsätze}}",
"empty": "Heute stehen keine Einsätze an.",
"allOrders": "Alle Aufträge ansehen"
},
"orders": {
"title": "Aufträge",
"tabs": {
"upcoming": "Kommend",
"running": "Laufend",
"to_complete": "Abzuschließen",
"past": "Vergangen"
},
"empty": "Keine Aufträge in dieser Liste."
},
"card": {
"open": "Auftrag öffnen",
"route": "Route",
"noDate": "Ohne Termin",
"emergency": "Notdienst",
"urgent": "Dringend",
"high": "Hoch"
},
"statusGroup": {
"new": "Neu",
"planned": "Geplant",
"en_route": "Unterwegs",
"in_progress": "In Arbeit",
"documentation_incomplete": "Dokumentation unvollständig",
"in_review": "Zur Prüfung",
"ready_for_billing": "Bereit zur Abrechnung",
"billed": "Abgerechnet",
"cancelled": "Storniert"
},
"statusDetail": {
"draft": "Entwurf",
"review_required": "Prüfung erforderlich",
"planned": "Geplant",
"assigned": "Zugewiesen",
"accepted": "Angenommen",
"en_route": "In Anfahrt",
"in_progress": "In Arbeit",
"paused": "Pausiert",
"waiting_material": "Wartet auf Material",
"daily_report_created": "Tagesbericht erstellt",
"technically_completed": "Technisch abgeschlossen",
"signature_pending": "Unterschrift ausstehend",
"in_review": "Zur Prüfung",
"released_for_billing": "Bereit zur Abrechnung",
"billed": "Abgerechnet",
"cancelled": "Storniert"
},
"action": {
"accept": "Annehmen",
"travel": "Losfahren",
"start": "Arbeit starten",
"pause": "Pause",
"resume": "Weiter",
"complete": "Abschließen",
"confirmComplete": "Jetzt abschließen",
"confirmHint": "Zeiterfassung wird beendet und der Auftrag technisch abgeschlossen.",
"saving": "Wird gespeichert …",
"blocked": "Vor dem Abschluss noch offen:",
"reportNext": "Bericht und Unterschrift folgen im nächsten Schritt.",
"nothing": "Keine Aktion erforderlich."
},
"blocker": {
"checklist_item": "Checkliste: {label}",
"photo_requirement": "Pflichtfoto: {label}",
"running_session": "Zeiterfassung eines Teammitglieds läuft noch",
"missing_field": "Angabe fehlt: {label}"
},
"errors": {
"not_found": "Nicht gefunden oder nicht freigegeben.",
"forbidden": "Keine Berechtigung für diese Aktion.",
"invalid": "Eingabe prüfen.",
"conflict": "Der Auftrag wurde inzwischen geändert. Die Ansicht ist aktualisiert.",
"blocked": "Pflichtangaben fehlen noch.",
"internal": "Technischer Fehler. Bitte erneut versuchen.",
"network": "Keine Verbindung. Bitte erneut versuchen.",
"image": "Foto konnte nicht verarbeitet werden."
},
"detail": {
"back": "Zurück",
"backToOrder": "Zum Auftrag",
"window": "Zeitfenster",
"hints": "Hinweise für Monteure",
"order": "Auftrag",
"orderType": "Auftragsart",
"externalNumber": "Kundenauftrag",
"team": "Team",
"scope": "Leistungsumfang",
"description": "Beschreibung",
"customer": "Kunde",
"contact": "Ansprechpartner",
"onSiteContact": "Vor Ort",
"call": "Anrufen",
"mail": "E-Mail",
"site": "Objekt",
"route": "Route öffnen",
"access": "Zugang",
"parking": "Parken",
"safety": "Sicherheit",
"technical": "Technik",
"documents": "Dokumente",
"noDocuments": "Keine Dokumente.",
"siteDocument": "Objekt",
"openDocument": "Öffnen",
"history": "Objekt-Historie",
"noHistory": "Keine freigegebenen Berichte am Objekt.",
"reportType": {
"daily": "Tagesbericht",
"completion": "Abschlussbericht"
},
"checklist": "Checkliste",
"checklistProgress": "{done} von {total} erledigt",
"materials": "Material",
"materialsProgress": "{done} von {total} Positionen erfasst",
"materialsNone": "Kein Material geplant",
"photos": "Fotos",
"photosProgress": "{count} Fotos · {done} von {total} Pflichtfotos",
"photosCount": "{count} Fotos",
"notes": "Notizen",
"notesCount": "{count, plural, =0 {Noch keine Notizen} one {# Notiz} other {# Notizen}}",
"time": "Zeiten",
"myTime": "Meine Arbeitszeit: {duration}",
"quick": {
"photo": "Foto",
"note": "Notiz",
"material": "Material",
"checklist": "Checkliste",
"time": "Zeiten"
}
},
"photos": {
"title": "Fotos",
"camera": "Kamera",
"gallery": "Galerie",
"phaseLabel": "Zeitpunkt",
"phase": {
"before": "Vorher",
"during": "Während",
"after": "Nachher"
},
"requirement": "Pflichtfoto",
"checklistItem": "Checklistenpunkt",
"none": "Keine Zuordnung",
"comment": "Kommentar",
"location": "Standort mitsenden",
"save": "Foto speichern",
"discard": "Verwerfen",
"preview": "Vorschau des Fotos",
"compressing": "Foto wird verkleinert …",
"uploading": "Hochladen … {percent} %",
"saved": "Foto gespeichert.",
"empty": "Noch keine Fotos.",
"requirements": "Pflichtfotos",
"missing": "Fehlt",
"done": "Vorhanden"
},
"notes": {
"title": "Notizen",
"kindLabel": "Art",
"kind": {
"work_done": "Durchgeführt",
"deviation": "Abweichung",
"problem": "Problem",
"additional_work": "Zusatzleistung",
"not_executable": "Nicht ausführbar",
"follow_up": "Folgearbeit",
"recommendation": "Empfehlung",
"customer_note": "Kundenhinweis",
"general": "Allgemein"
},
"text": "Notiz",
"placeholder": "Kurz beschreiben, was vor Ort passiert ist …",
"save": "Notiz speichern",
"saved": "Notiz gespeichert.",
"draftKept": "Entwurf bleibt auf diesem Gerät erhalten.",
"empty": "Noch keine Notizen."
},
"voice": {
"title": "Sprachnotiz",
"record": "Aufnahme starten",
"stop": "Aufnahme beenden",
"recording": "Aufnahme läuft · {time} von 5:00",
"save": "Sprachnotiz speichern",
"discard": "Verwerfen",
"saved": "Sprachnotiz gespeichert.",
"unsupported": "Sprachaufnahme wird auf diesem Gerät nicht unterstützt.",
"denied": "Mikrofon ist nicht freigegeben.",
"status": {
"pending": "Transkription läuft",
"done": "Transkribiert",
"failed": "Transkription fehlgeschlagen",
"disabled": "Ohne Transkription"
}
},
"materials": {
"title": "Material",
"planned": "Geplant: {quantity} {unit}",
"usage": {
"fully_used": "Vollständig",
"partially_used": "Teilweise",
"not_used": "Nicht verwendet",
"additional": "Zusätzlich"
},
"quantity": "Menge",
"unit": "Einheit",
"decrease": "Menge verringern",
"increase": "Menge erhöhen",
"reason": "Begründung der Abweichung",
"reasonRequired": "Bei Abweichung ist eine Begründung Pflicht.",
"save": "Speichern",
"saved": "Gespeichert.",
"recorded": "Erfasst: {quantity} {unit}",
"open": "Noch offen",
"noPlan": "Kein Material geplant.",
"additionalTitle": "Zusatzmaterial",
"additionalEmpty": "Kein Zusatzmaterial erfasst.",
"name": "Bezeichnung",
"articleNumber": "Artikelnummer (optional)",
"additionalReason": "Grund",
"add": "Zusatzmaterial speichern",
"invalid": "Angaben unvollständig oder Menge passt nicht zum Status."
},
"checklist": {
"title": "Checkliste",
"required": "Pflicht",
"photo": "Foto nötig",
"done": "Erledigt",
"open": "Offen",
"comment": "Kommentar",
"saveComment": "Kommentar speichern",
"empty": "Für diesen Auftrag gibt es keine Checkliste."
},
"time": {
"title": "Zeiten",
"type": {
"travel": "Anfahrt",
"work": "Arbeitszeit",
"break": "Pause",
"material_procurement": "Materialbeschaffung",
"return_travel": "Rückfahrt",
"interruption": "Unterbrechung"
},
"session": {
"en_route": "In Anfahrt",
"running": "Läuft",
"paused": "Pausiert",
"ended": "Beendet"
},
"running": "läuft",
"offlineStarted": "offline gestartet",
"corrected": "Korrigiert: {reason}",
"correct": "Korrigieren",
"start": "Beginn",
"end": "Ende",
"reason": "Grund der Korrektur",
"reasonHint": "Pflichtangabe, wird protokolliert.",
"save": "Korrektur speichern",
"saved": "Korrektur gespeichert.",
"empty": "Noch keine Zeiten erfasst.",
"totalWork": "Arbeitszeit gesamt: {duration}"
},
"profile": {
"title": "Profil",
"tenant": "Betrieb",
"teams": "Team",
"noTeam": "Keinem Team zugeordnet",
"roles": "Rolle",
"language": "Sprache",
"logout": "Abmelden",
"backoffice": "Zum Büro-Bereich"
},
"sync": {
"title": "Synchronisation",
"immediate": "Änderungen werden sofort an das Büro übertragen.",
"offlineHint": "Ohne Verbindung können Eingaben noch nicht zwischengespeichert werden – die Offline-Warteschlange folgt.",
"status": "Verbindung"
}
}
+298
View File
@@ -0,0 +1,298 @@
{
"nav": {
"label": "Field navigation",
"today": "Today",
"orders": "Orders",
"emergency": "Emergency",
"sync": "Sync",
"profile": "Profile"
},
"connection": {
"online": "Online",
"offline": "Offline"
},
"noAccess": "The field app is not enabled for your role.",
"today": {
"title": "Today",
"greeting": "Hello {name}",
"count": "{count, plural, =0 {No jobs} one {# job} other {# jobs}}",
"empty": "No jobs scheduled for today.",
"allOrders": "View all orders"
},
"orders": {
"title": "Orders",
"tabs": {
"upcoming": "Upcoming",
"running": "Ongoing",
"to_complete": "To complete",
"past": "Past"
},
"empty": "No orders in this list."
},
"card": {
"open": "Open order",
"route": "Route",
"noDate": "No date",
"emergency": "Emergency",
"urgent": "Urgent",
"high": "High"
},
"statusGroup": {
"new": "New",
"planned": "Planned",
"en_route": "On the way",
"in_progress": "In progress",
"documentation_incomplete": "Documentation incomplete",
"in_review": "In review",
"ready_for_billing": "Ready for billing",
"billed": "Billed",
"cancelled": "Cancelled"
},
"statusDetail": {
"draft": "Draft",
"review_required": "Review required",
"planned": "Planned",
"assigned": "Assigned",
"accepted": "Accepted",
"en_route": "On the way",
"in_progress": "In progress",
"paused": "Paused",
"waiting_material": "Waiting for material",
"daily_report_created": "Daily report created",
"technically_completed": "Technically completed",
"signature_pending": "Signature pending",
"in_review": "In review",
"released_for_billing": "Ready for billing",
"billed": "Billed",
"cancelled": "Cancelled"
},
"action": {
"accept": "Accept",
"travel": "Leave now",
"start": "Start work",
"pause": "Pause",
"resume": "Resume",
"complete": "Complete",
"confirmComplete": "Complete now",
"confirmHint": "Time tracking stops and the order is marked technically completed.",
"saving": "Saving …",
"blocked": "Still missing before completion:",
"reportNext": "Report and signature follow in the next step.",
"nothing": "No action required."
},
"blocker": {
"checklist_item": "Checklist: {label}",
"photo_requirement": "Required photo: {label}",
"running_session": "A team member's time tracking is still running",
"missing_field": "Missing: {label}"
},
"errors": {
"not_found": "Not found or not shared with you.",
"forbidden": "You are not allowed to do this.",
"invalid": "Please check your input.",
"conflict": "The order was changed in the meantime. The view has been refreshed.",
"blocked": "Required information is still missing.",
"internal": "Technical error. Please try again.",
"network": "No connection. Please try again.",
"image": "The photo could not be processed."
},
"detail": {
"back": "Back",
"backToOrder": "Back to order",
"window": "Time window",
"hints": "Notes for technicians",
"order": "Order",
"orderType": "Order type",
"externalNumber": "Customer order",
"team": "Team",
"scope": "Scope of work",
"description": "Description",
"customer": "Customer",
"contact": "Contact",
"onSiteContact": "On site",
"call": "Call",
"mail": "Email",
"site": "Site",
"route": "Open route",
"access": "Access",
"parking": "Parking",
"safety": "Safety",
"technical": "Technical",
"documents": "Documents",
"noDocuments": "No documents.",
"siteDocument": "Site",
"openDocument": "Open",
"history": "Site history",
"noHistory": "No approved reports at this site.",
"reportType": {
"daily": "Daily report",
"completion": "Completion report"
},
"checklist": "Checklist",
"checklistProgress": "{done} of {total} done",
"materials": "Material",
"materialsProgress": "{done} of {total} items recorded",
"materialsNone": "No material planned",
"photos": "Photos",
"photosProgress": "{count} photos · {done} of {total} required photos",
"photosCount": "{count} photos",
"notes": "Notes",
"notesCount": "{count, plural, =0 {No notes yet} one {# note} other {# notes}}",
"time": "Time",
"myTime": "My working time: {duration}",
"quick": {
"photo": "Photo",
"note": "Note",
"material": "Material",
"checklist": "Checklist",
"time": "Time"
}
},
"photos": {
"title": "Photos",
"camera": "Camera",
"gallery": "Gallery",
"phaseLabel": "When",
"phase": {
"before": "Before",
"during": "During",
"after": "After"
},
"requirement": "Required photo",
"checklistItem": "Checklist item",
"none": "Not assigned",
"comment": "Comment",
"location": "Include location",
"save": "Save photo",
"discard": "Discard",
"preview": "Photo preview",
"compressing": "Resizing photo …",
"uploading": "Uploading … {percent} %",
"saved": "Photo saved.",
"empty": "No photos yet.",
"requirements": "Required photos",
"missing": "Missing",
"done": "Present"
},
"notes": {
"title": "Notes",
"kindLabel": "Type",
"kind": {
"work_done": "Work done",
"deviation": "Deviation",
"problem": "Problem",
"additional_work": "Additional work",
"not_executable": "Not feasible",
"follow_up": "Follow-up work",
"recommendation": "Recommendation",
"customer_note": "Customer note",
"general": "General"
},
"text": "Note",
"placeholder": "Briefly describe what happened on site …",
"save": "Save note",
"saved": "Note saved.",
"draftKept": "Draft is kept on this device.",
"empty": "No notes yet."
},
"voice": {
"title": "Voice note",
"record": "Start recording",
"stop": "Stop recording",
"recording": "Recording · {time} of 5:00",
"save": "Save voice note",
"discard": "Discard",
"saved": "Voice note saved.",
"unsupported": "Voice recording is not supported on this device.",
"denied": "Microphone access is not granted.",
"status": {
"pending": "Transcription running",
"done": "Transcribed",
"failed": "Transcription failed",
"disabled": "No transcription"
}
},
"materials": {
"title": "Material",
"planned": "Planned: {quantity} {unit}",
"usage": {
"fully_used": "Fully used",
"partially_used": "Partially used",
"not_used": "Not used",
"additional": "Additional"
},
"quantity": "Quantity",
"unit": "Unit",
"decrease": "Decrease quantity",
"increase": "Increase quantity",
"reason": "Reason for deviation",
"reasonRequired": "A reason is required for deviations.",
"save": "Save",
"saved": "Saved.",
"recorded": "Recorded: {quantity} {unit}",
"open": "Open",
"noPlan": "No material planned.",
"additionalTitle": "Additional material",
"additionalEmpty": "No additional material recorded.",
"name": "Name",
"articleNumber": "Article number (optional)",
"additionalReason": "Reason",
"add": "Save additional material",
"invalid": "Incomplete input or quantity does not match the status."
},
"checklist": {
"title": "Checklist",
"required": "Required",
"photo": "Photo needed",
"done": "Done",
"open": "Open",
"comment": "Comment",
"saveComment": "Save comment",
"empty": "This order has no checklist."
},
"time": {
"title": "Time",
"type": {
"travel": "Travel",
"work": "Working time",
"break": "Break",
"material_procurement": "Material procurement",
"return_travel": "Return travel",
"interruption": "Interruption"
},
"session": {
"en_route": "On the way",
"running": "Running",
"paused": "Paused",
"ended": "Ended"
},
"running": "running",
"offlineStarted": "started offline",
"corrected": "Corrected: {reason}",
"correct": "Correct",
"start": "Start",
"end": "End",
"reason": "Reason for correction",
"reasonHint": "Required, will be logged.",
"save": "Save correction",
"saved": "Correction saved.",
"empty": "No time recorded yet.",
"totalWork": "Total working time: {duration}"
},
"profile": {
"title": "Profile",
"tenant": "Company",
"teams": "Team",
"noTeam": "Not assigned to a team",
"roles": "Role",
"language": "Language",
"logout": "Sign out",
"backoffice": "Go to office area"
},
"sync": {
"title": "Synchronisation",
"immediate": "Changes are sent to the office immediately.",
"offlineHint": "Without a connection, entries cannot be buffered yet – the offline queue will follow.",
"status": "Connection"
}
}