{t("title")}
+{t("sub")}
+diff --git a/messages/de/emergency.json b/messages/de/emergency.json
new file mode 100644
index 0000000..189ad4e
--- /dev/null
+++ b/messages/de/emergency.json
@@ -0,0 +1,236 @@
+{
+ "capture": {
+ "title": "Notdienst",
+ "sub": "Einsatz in 3 Schritten anlegen und sofort starten.",
+ "noAccess": "Die Notdienst-Erfassung ist für Ihre Rolle nicht freigeschaltet.",
+ "stepOf": "Schritt {step} von 3",
+ "steps": {
+ "customer": "Kunde",
+ "site": "Einsatzort",
+ "reason": "Grund & Start"
+ },
+ "next": "Weiter",
+ "back": "Zurück",
+ "required": "Bitte die markierten Pflichtfelder ausfüllen.",
+ "start": "Einsatz starten",
+ "starting": "Einsatz wird angelegt …",
+ "queued": "Einsatz ist gespeichert und wird übertragen, sobald wieder Verbindung besteht.",
+ "customer": {
+ "existing": "Bestehender Kunde",
+ "new": "Neuer Kunde",
+ "search": "Kunde suchen",
+ "searchPlaceholder": "Name, Ort oder Kundennummer",
+ "searchHint": "Mindestens 2 Zeichen eingeben.",
+ "searching": "Suche läuft …",
+ "offline": "Suche gerade nicht möglich. Ohne Verbindung bitte einen neuen Kunden anlegen.",
+ "noHits": "Kein Kunde gefunden. Legen Sie einen neuen Kunden an.",
+ "provisional": "Vorläufig",
+ "change": "Anderen Kunden wählen",
+ "selectRequired": "Bitte einen Kunden auswählen oder neu anlegen.",
+ "newHint": "Wird als vorläufiger Kunde angelegt. Das Büro prüft ihn am nächsten Arbeitstag.",
+ "companyName": "Firma",
+ "firstName": "Vorname",
+ "lastName": "Nachname",
+ "nameHint": "Firma oder Nachname angeben.",
+ "phone": "Telefon",
+ "email": "E-Mail (optional)",
+ "street": "Straße",
+ "houseNumber": "Nr.",
+ "postalCode": "PLZ",
+ "city": "Ort"
+ },
+ "site": {
+ "loading": "Objekte werden geladen …",
+ "existing": "Objekt des Kunden",
+ "newAddress": "Andere Einsatzadresse",
+ "none": "Für diesen Kunden ist kein Objekt hinterlegt. Bitte die Einsatzadresse erfassen.",
+ "newHint": "Die Einsatzadresse wird als vorläufiges Objekt angelegt.",
+ "useCustomerAddress": "Kundenadresse übernehmen",
+ "name": "Bezeichnung (optional)",
+ "contactTitle": "Ansprechpartner vor Ort",
+ "contactName": "Name",
+ "contactPhone": "Telefon"
+ },
+ "reason": {
+ "label": "Grund des Notdiensteinsatzes",
+ "placeholder": "z. B. Wasserrohrbruch im Keller, Heizung ausgefallen",
+ "voice": "Sprachnotiz (optional)",
+ "voiceRecord": "Sprachnotiz aufnehmen",
+ "voiceRecording": "Aufnahme läuft · {seconds} s",
+ "voiceStop": "Aufnahme beenden",
+ "voiceDiscard": "Aufnahme verwerfen",
+ "voiceHint": "Wird nach dem Start am Einsatz gespeichert.",
+ "voiceDenied": "Kein Zugriff auf das Mikrofon. Bitte in den Geräteeinstellungen erlauben.",
+ "startedAt": "Beginn",
+ "team": "Team",
+ "noTeam": "Ohne Team",
+ "technicians": "Monteure",
+ "me": "Ich (immer dabei)"
+ },
+ "errors": {
+ "not_found": "Kunde oder Objekt nicht gefunden. Bitte Auswahl prüfen.",
+ "forbidden": "Dafür fehlt Ihnen die Berechtigung.",
+ "invalid": "Bitte die Eingaben prüfen.",
+ "conflict": "Der Einsatz wurde bereits angelegt. Bitte die Auftragsliste prüfen.",
+ "blocked": "Der Einsatz kann gerade nicht angelegt werden.",
+ "internal": "Einsatz konnte nicht angelegt werden. Bitte erneut versuchen.",
+ "network": "Keine Verbindung. Bitte erneut auf „Einsatz starten“ tippen."
+ }
+ },
+ "review": {
+ "crumb": "Aufträge · Notdienst",
+ "title": "Notdienst-Prüfung",
+ "sub": "Notdiensteinsätze mit vorläufigen Stammdaten oder zur Prüfung.",
+ "filterLabel": "Filter Notdiensteinsätze",
+ "filter": {
+ "open": "Zu prüfen",
+ "all": "Alle Notdienste"
+ },
+ "emptyOpen": "Keine Notdiensteinsätze zu prüfen.",
+ "emptyAll": "Noch keine Notdiensteinsätze.",
+ "customer": "Kunde",
+ "site": "Objekt",
+ "technician": "Monteur",
+ "startedAt": "Beginn",
+ "provisional": "Vorläufig",
+ "progress": "{count} von {total} Prüfschritten erledigt",
+ "back": "Zur Notdienst-Prüfung",
+ "detailSub": "Monteur: {technician} · Einsatzbeginn: {start} · Einsatzende: {end}",
+ "running": "läuft noch",
+ "openOrder": "Auftrag öffnen",
+ "steps": {
+ "customer": "Kunde klären",
+ "site": "Objekt klären",
+ "order": "Auftrag ergänzen",
+ "report": "Bericht freigeben",
+ "billing": "Abrechnung freigeben"
+ },
+ "stepDone": "Erledigt",
+ "stepOpen": "Offen",
+ "reason": "Grund des Notdiensteinsatzes",
+ "customerName": "Name",
+ "customerNumber": "Kundennummer",
+ "numberOnConfirm": "wird bei Bestätigung vergeben",
+ "status": "Status",
+ "customerStatus": {
+ "active": "Aktiv",
+ "inactive": "Inaktiv",
+ "provisional": "Vorläufig – Prüfung durch Backoffice erforderlich",
+ "merged": "Zusammengeführt"
+ },
+ "phone": "Telefon",
+ "email": "E-Mail",
+ "address": "Adresse",
+ "contact": "Ansprechpartner",
+ "provisionalHint": "Vorläufiger Kunde: bestätigen, einem bestehenden Kunden zuordnen oder mit einer Dublette zusammenführen.",
+ "saved": "Gespeichert.",
+ "confirmCustomer": {
+ "title": "Kunden bestätigen",
+ "hint": "Der Kunde wird aktiv und erhält eine Kundennummer.",
+ "submit": "Kunden bestätigen"
+ },
+ "duplicates": {
+ "title": "Mögliche Dubletten",
+ "none": "Keine ähnlichen Kunden gefunden.",
+ "score": "Übereinstimmung {score} %"
+ },
+ "target": {
+ "select": "Bestehender Kunde",
+ "none": "Bitte wählen",
+ "number": "oder Kundennummer"
+ },
+ "assignCustomer": {
+ "title": "Bestehendem Kunden zuordnen",
+ "hint": "Auftrag, Objekt und Ansprechpartner werden zum gewählten Kunden umgehängt. Der vorläufige Datensatz wird als zusammengeführt markiert.",
+ "submit": "Zuordnen"
+ },
+ "mergeCustomer": {
+ "title": "Dublette zusammenführen",
+ "hint": "Alle Ansprechpartner, Objekte, Aufträge und Dokumente dieses Kunden gehen auf den gewählten Kunden über. Nicht umkehrbar.",
+ "confirm": "Ich habe geprüft, dass es derselbe Kunde ist.",
+ "submit": "Zusammenführen"
+ },
+ "siteName": "Bezeichnung",
+ "siteStatus": {
+ "active": "Aktiv",
+ "inactive": "Inaktiv",
+ "provisional": "Vorläufig – Prüfung durch Backoffice erforderlich"
+ },
+ "onSiteContact": "Ansprechpartner vor Ort",
+ "noSite": "Kein Objekt am Auftrag.",
+ "street": "Straße",
+ "houseNumber": "Nr.",
+ "postalCode": "PLZ",
+ "city": "Ort",
+ "accessNotes": "Zugangshinweise",
+ "correctSite": {
+ "title": "Objekt korrigieren",
+ "confirm": "Objekt bestätigen (vorläufig → aktiv)",
+ "submit": "Objekt speichern"
+ },
+ "assignSite": {
+ "title": "Bestehendes Objekt zuordnen",
+ "hint": "Der Auftrag wird dem gewählten Objekt zugeordnet. Ein nicht mehr genutztes vorläufiges Objekt wird entfernt.",
+ "select": "Objekt des Kunden",
+ "none": "Der Kunde hat keine weiteren Objekte.",
+ "submit": "Objekt zuordnen"
+ },
+ "completeOrder": {
+ "title": "Titel",
+ "description": "Beschreibung",
+ "orderType": "Auftragsart",
+ "billingType": "Abrechnungsart",
+ "choose": "Bitte wählen",
+ "hint": "Erledigt, sobald Auftragsart und Abrechnungsart gesetzt sind.",
+ "submit": "Auftrag speichern"
+ },
+ "billingTypes": {
+ "fixed": "Pauschal",
+ "time_material": "Nach Aufwand",
+ "maintenance_contract": "Wartungsvertrag",
+ "warranty": "Gewährleistung"
+ },
+ "report": {
+ "none": "Noch kein Bericht eingereicht.",
+ "open": "Bericht prüfen",
+ "hint": "Freigabe und Zurückweisung erfolgen auf der Berichtsseite.",
+ "version": "Version {version}",
+ "type": {
+ "daily": "Tagesbericht",
+ "completion": "Abschlussbericht"
+ },
+ "status": {
+ "draft": "Entwurf",
+ "submitted": "Eingereicht",
+ "team_approved": "Vom Teamleiter geprüft",
+ "approved": "Freigegeben",
+ "rejected": "Zurückgewiesen",
+ "superseded": "Ersetzt"
+ }
+ },
+ "billing": {
+ "hint": "Möglich, wenn Kunde und Objekt geklärt sind und der Abschlussbericht freigegeben ist.",
+ "submit": "Zur Abrechnung freigeben",
+ "current": "Aktueller Status"
+ },
+ "errors": {
+ "generic": "Das hat nicht geklappt. Bitte erneut versuchen.",
+ "invalid": "Bitte die Eingaben prüfen.",
+ "not_found": "Nicht gefunden.",
+ "forbidden": "Dafür fehlt Ihnen die Berechtigung.",
+ "conflict": "Der Datensatz wurde inzwischen geändert. Bitte Seite neu laden.",
+ "blocked": "Noch nicht möglich: Voraussetzungen fehlen.",
+ "not_provisional": "Der Kunde ist nicht mehr vorläufig.",
+ "same_customer": "Bitte einen anderen Kunden wählen.",
+ "same_site": "Der Auftrag ist diesem Objekt bereits zugeordnet.",
+ "target_not_found": "Kunde nicht gefunden. Bitte Auswahl oder Kundennummer prüfen.",
+ "not_found_target": "Kunde nicht gefunden.",
+ "confirm_required": "Bitte die Prüfung bestätigen.",
+ "already_merged": "Einer der Kunden wurde bereits zusammengeführt.",
+ "no_site": "Der Auftrag hat kein Objekt.",
+ "master_data_open": "Erst Kunde und Objekt klären, dann zur Abrechnung freigeben.",
+ "number_allocation": "Kundennummer konnte nicht vergeben werden. Bitte erneut versuchen.",
+ "number_taken": "Diese Kundennummer ist bereits vergeben."
+ }
+ }
+}
diff --git a/messages/en/emergency.json b/messages/en/emergency.json
new file mode 100644
index 0000000..a8f8460
--- /dev/null
+++ b/messages/en/emergency.json
@@ -0,0 +1,236 @@
+{
+ "capture": {
+ "title": "Emergency call-out",
+ "sub": "Create the call-out in 3 steps and start right away.",
+ "noAccess": "Emergency capture is not enabled for your role.",
+ "stepOf": "Step {step} of 3",
+ "steps": {
+ "customer": "Customer",
+ "site": "Location",
+ "reason": "Reason & start"
+ },
+ "next": "Next",
+ "back": "Back",
+ "required": "Please fill in the marked required fields.",
+ "start": "Start call-out",
+ "starting": "Creating call-out …",
+ "queued": "Call-out saved. It will be transferred once you are back online.",
+ "customer": {
+ "existing": "Existing customer",
+ "new": "New customer",
+ "search": "Search customer",
+ "searchPlaceholder": "Name, city or customer number",
+ "searchHint": "Enter at least 2 characters.",
+ "searching": "Searching …",
+ "offline": "Search is not available right now. Without a connection, please create a new customer.",
+ "noHits": "No customer found. Create a new customer.",
+ "provisional": "Provisional",
+ "change": "Choose another customer",
+ "selectRequired": "Please select a customer or create a new one.",
+ "newHint": "Will be created as a provisional customer. The office reviews it on the next working day.",
+ "companyName": "Company",
+ "firstName": "First name",
+ "lastName": "Last name",
+ "nameHint": "Enter company or last name.",
+ "phone": "Phone",
+ "email": "E-mail (optional)",
+ "street": "Street",
+ "houseNumber": "No.",
+ "postalCode": "Postcode",
+ "city": "City"
+ },
+ "site": {
+ "loading": "Loading sites …",
+ "existing": "Customer site",
+ "newAddress": "Other call-out address",
+ "none": "No site on file for this customer. Please enter the call-out address.",
+ "newHint": "The call-out address is created as a provisional site.",
+ "useCustomerAddress": "Use customer address",
+ "name": "Label (optional)",
+ "contactTitle": "Contact on site",
+ "contactName": "Name",
+ "contactPhone": "Phone"
+ },
+ "reason": {
+ "label": "Reason for the emergency call-out",
+ "placeholder": "e.g. burst water pipe in the basement, heating failure",
+ "voice": "Voice note (optional)",
+ "voiceRecord": "Record voice note",
+ "voiceRecording": "Recording · {seconds} s",
+ "voiceStop": "Stop recording",
+ "voiceDiscard": "Discard recording",
+ "voiceHint": "Saved to the call-out after it has started.",
+ "voiceDenied": "No microphone access. Please allow it in the device settings.",
+ "startedAt": "Start",
+ "team": "Team",
+ "noTeam": "No team",
+ "technicians": "Technicians",
+ "me": "Me (always included)"
+ },
+ "errors": {
+ "not_found": "Customer or site not found. Please check your selection.",
+ "forbidden": "You do not have permission for this.",
+ "invalid": "Please check your entries.",
+ "conflict": "The call-out has already been created. Please check the order list.",
+ "blocked": "The call-out cannot be created right now.",
+ "internal": "The call-out could not be created. Please try again.",
+ "network": "No connection. Please tap “Start call-out” again."
+ }
+ },
+ "review": {
+ "crumb": "Work orders · Emergency",
+ "title": "Emergency review",
+ "sub": "Emergency call-outs with provisional master data or awaiting review.",
+ "filterLabel": "Filter emergency call-outs",
+ "filter": {
+ "open": "To review",
+ "all": "All call-outs"
+ },
+ "emptyOpen": "No emergency call-outs to review.",
+ "emptyAll": "No emergency call-outs yet.",
+ "customer": "Customer",
+ "site": "Site",
+ "technician": "Technician",
+ "startedAt": "Start",
+ "provisional": "Provisional",
+ "progress": "{count} of {total} review steps done",
+ "back": "Back to emergency review",
+ "detailSub": "Technician: {technician} · Start: {start} · End: {end}",
+ "running": "still running",
+ "openOrder": "Open work order",
+ "steps": {
+ "customer": "Clarify customer",
+ "site": "Clarify site",
+ "order": "Complete order",
+ "report": "Approve report",
+ "billing": "Release for billing"
+ },
+ "stepDone": "Done",
+ "stepOpen": "Open",
+ "reason": "Reason for the emergency call-out",
+ "customerName": "Name",
+ "customerNumber": "Customer number",
+ "numberOnConfirm": "assigned on confirmation",
+ "status": "Status",
+ "customerStatus": {
+ "active": "Active",
+ "inactive": "Inactive",
+ "provisional": "Provisional – review by back office required",
+ "merged": "Merged"
+ },
+ "phone": "Phone",
+ "email": "E-mail",
+ "address": "Address",
+ "contact": "Contact",
+ "provisionalHint": "Provisional customer: confirm, assign to an existing customer or merge with a duplicate.",
+ "saved": "Saved.",
+ "confirmCustomer": {
+ "title": "Confirm customer",
+ "hint": "The customer becomes active and receives a customer number.",
+ "submit": "Confirm customer"
+ },
+ "duplicates": {
+ "title": "Possible duplicates",
+ "none": "No similar customers found.",
+ "score": "Match {score} %"
+ },
+ "target": {
+ "select": "Existing customer",
+ "none": "Please choose",
+ "number": "or customer number"
+ },
+ "assignCustomer": {
+ "title": "Assign to existing customer",
+ "hint": "Order, site and contacts move to the chosen customer. The provisional record is marked as merged.",
+ "submit": "Assign"
+ },
+ "mergeCustomer": {
+ "title": "Merge duplicate",
+ "hint": "All contacts, sites, orders and documents of this customer move to the chosen customer. Cannot be undone.",
+ "confirm": "I have checked that this is the same customer.",
+ "submit": "Merge"
+ },
+ "siteName": "Label",
+ "siteStatus": {
+ "active": "Active",
+ "inactive": "Inactive",
+ "provisional": "Provisional – review by back office required"
+ },
+ "onSiteContact": "Contact on site",
+ "noSite": "No site on this order.",
+ "street": "Street",
+ "houseNumber": "No.",
+ "postalCode": "Postcode",
+ "city": "City",
+ "accessNotes": "Access notes",
+ "correctSite": {
+ "title": "Correct site",
+ "confirm": "Confirm site (provisional → active)",
+ "submit": "Save site"
+ },
+ "assignSite": {
+ "title": "Assign existing site",
+ "hint": "The order is assigned to the chosen site. A provisional site that is no longer used is removed.",
+ "select": "Customer site",
+ "none": "The customer has no other sites.",
+ "submit": "Assign site"
+ },
+ "completeOrder": {
+ "title": "Title",
+ "description": "Description",
+ "orderType": "Order type",
+ "billingType": "Billing type",
+ "choose": "Please choose",
+ "hint": "Done once order type and billing type are set.",
+ "submit": "Save order"
+ },
+ "billingTypes": {
+ "fixed": "Fixed price",
+ "time_material": "Time and material",
+ "maintenance_contract": "Maintenance contract",
+ "warranty": "Warranty"
+ },
+ "report": {
+ "none": "No report submitted yet.",
+ "open": "Review report",
+ "hint": "Approval and rejection happen on the report page.",
+ "version": "Version {version}",
+ "type": {
+ "daily": "Daily report",
+ "completion": "Completion report"
+ },
+ "status": {
+ "draft": "Draft",
+ "submitted": "Submitted",
+ "team_approved": "Checked by team lead",
+ "approved": "Approved",
+ "rejected": "Rejected",
+ "superseded": "Superseded"
+ }
+ },
+ "billing": {
+ "hint": "Possible once customer and site are clarified and the completion report is approved.",
+ "submit": "Release for billing",
+ "current": "Current status"
+ },
+ "errors": {
+ "generic": "That did not work. Please try again.",
+ "invalid": "Please check your entries.",
+ "not_found": "Not found.",
+ "forbidden": "You do not have permission for this.",
+ "conflict": "The record has changed in the meantime. Please reload the page.",
+ "blocked": "Not possible yet: prerequisites are missing.",
+ "not_provisional": "The customer is no longer provisional.",
+ "same_customer": "Please choose a different customer.",
+ "same_site": "The order is already assigned to this site.",
+ "target_not_found": "Customer not found. Please check the selection or customer number.",
+ "not_found_target": "Customer not found.",
+ "confirm_required": "Please confirm the check.",
+ "already_merged": "One of the customers has already been merged.",
+ "no_site": "The order has no site.",
+ "master_data_open": "Clarify customer and site first, then release for billing.",
+ "number_allocation": "The customer number could not be assigned. Please try again.",
+ "number_taken": "This customer number is already taken."
+ }
+ }
+}
diff --git a/src/app/(field)/m/emergency/page.tsx b/src/app/(field)/m/emergency/page.tsx
index 5064860..f96b880 100644
--- a/src/app/(field)/m/emergency/page.tsx
+++ b/src/app/(field)/m/emergency/page.tsx
@@ -1,5 +1,25 @@
-import { ModulePlaceholder } from "@/components/module-placeholder";
+import { getTranslations } from "next-intl/server";
+import { moduleGuard } from "@/server/action-guard";
+import { can, ctxFromGuard } from "@/server/services/context";
+import { emergencyTeamOptions } from "@/server/services/emergency/lookup";
+import { EmergencyWizard } from "@/components/emergency/emergency-wizard";
-export default function Page() {
- return
{t("noAccess")}
; + } + const teams = await emergencyTeamOptions(ctx); + + return ( +{t("sub")}
++ {t("stepOf", { step })}: {stepLabels[step - 1]} +
+ + {step === 1 && ( +{selected.name}
++ {[selected.customerNumber, selected.city].filter(Boolean).join(" · ")} + {selected.provisional && ` · ${t("customer.provisional")}`} +
+{t("customer.searchHint")}
+
+
{t("customer.offline")}
} + {!searching && query.trim().length >= 2 && hits.length === 0 && !searchFailed && ( +{t("customer.noHits")}
+ )} +{t("customer.selectRequired")}
} +{t("customer.newHint")}
+{t("customer.nameHint")}
+{t("required")}
} +
+
{t("site.none")}
} + + {siteMode === "new" && (sites !== null || customerMode === "new") && ( +{t("site.newHint")}
+ {customerMode === "new" && (newCustomer.street || newCustomer.city) && ( + + )} +{t("required")}
} + +{t("reason.voice")}
+ {recState === "idle" && ( + + )} + {recState === "recording" && ( + <> ++ + {t("reason.voiceRecording", { seconds: recSeconds })} +
+ + > + )} + {recState === "recorded" && clip && ( + <> + + + > + )} +{t("reason.voiceHint")}
+{t("required")}
} + {error && ( +
+