diff --git a/messages/de/field.json b/messages/de/field.json index c1b3f21..f4cff5d 100644 --- a/messages/de/field.json +++ b/messages/de/field.json @@ -6,7 +6,8 @@ "emergency": "Notdienst", "sync": "Sync", "profile": "Profil", - "approvalsBadge": "{count} Zeiten zur Freigabe" + "approvalsBadge": "{count} Zeiten zur Freigabe", + "lotse": "Lotse" }, "connection": { "online": "Online", diff --git a/messages/de/lotse.json b/messages/de/lotse.json index 313ff19..be3cd64 100644 --- a/messages/de/lotse.json +++ b/messages/de/lotse.json @@ -89,6 +89,8 @@ "use": "Verwendung", "enabled": "Lotse für diesen Betrieb verwenden", "enabledHint": "Ausgeschaltet: keine Berichtsentwürfe, keine Zusammenfassungen, Sprachnotizen ohne Transkription.", + "chatEnabled": "Lotse-Chat für Monteure", + "chatEnabledHint": "Monteure schreiben oder sprechen mit dem Lotsen. Aktionen (Status, Zeiten, Material, Notizen) werden nur nach Bestätigung durch den Monteur ausgeführt.", "addressForm": "Anrede des Lotsen", "addressFormHint": "Gilt einheitlich für alle Texte des Lotsen in diesem Betrieb.", "address": { @@ -114,7 +116,9 @@ "order": "Auftragstitel, Beschreibung und Leistungsumfang", "notes": "Tätigkeitsnotizen und Texte aus Sprachnotizen", "work": "Checkliste, Material mit Abweichungsgründen, Fotokommentare, Arbeitszeiten", - "audio": "Für die Transkription: die Audiodatei der Sprachnotiz" + "audio": "Für die Transkription: die Audiodatei der Sprachnotiz", + "chat": "Lotse-Chat: Nachrichten des Monteurs (Telefonnummern, E-Mail-Adressen und Anschriften ersetzt) und die Auftragsdaten oben. Kundennamen und Kontaktdaten nur als Platzhalter – eingesetzt wird erst in der Antwort auf dem Gerät des Monteurs.", + "chatAudio": "Spracheingabe im Chat: die Aufnahme zur Transkription (wird nicht gespeichert)" }, "notSentTitle": "Nicht gesendet wird", "notSent": { @@ -152,7 +156,172 @@ "voice_summary": "Zusammenfassung Sprachnotiz", "transcription": "Transkription", "import_extraction": "Auftragsimport", - "completeness_check": "Vollständigkeitsprüfung" + "completeness_check": "Vollständigkeitsprüfung", + "lotse_chat": "Lotse-Chat" + } + }, + "chat": { + "title": "Lotse-Chat", + "intro": "Kurz schreiben oder sprechen, was erledigt ist – z. B. „Auftrag fertig, Speicher installiert, 2 Std., 1 Filter verbaut“. Der Lotse schlägt Aktionen vor. Gespeichert wird erst nach Bestätigung.", + "context": "Auftrag {number} · {title}", + "noContext": "Ohne Auftragsbezug", + "ask": "Lotse fragen", + "input": "Nachricht an den Lotsen", + "send": "Senden", + "sending": "Lotse denkt nach …", + "newChat": "Neuer Chat", + "userLabel": "Eigene Nachricht", + "lotseLabel": "Lotse", + "empty": "Noch keine Nachrichten.", + "notConfigured": "Lotse ist nicht eingerichtet. Das Büro kann den KI-Anbieter hinterlegen.", + "offline": "Keine Verbindung. Der Lotse braucht Internet – die Eingabe bleibt erhalten.", + "chipsLabel": "Antwortvorschläge", + "linksLabel": "Direkt ergänzen", + "confirmAll": "Alle bestätigen ({count})", + "mic": { + "record": "Sprechen", + "stop": "Aufnahme beenden", + "recording": "Aufnahme läuft · {time}", + "transcribing": "Wird in Text umgewandelt …", + "unavailable": "Spracheingabe ist nicht eingerichtet.", + "denied": "Kein Zugriff auf das Mikrofon.", + "unsupported": "Spracheingabe wird auf diesem Gerät nicht unterstützt.", + "failed": "Umwandlung in Text fehlgeschlagen. Bitte tippen.", + "hint": "Text vor dem Senden prüfen." + }, + "card": { + "confirm": "Bestätigen", + "edit": "Bearbeiten", + "discard": "Verwerfen", + "saveConfirm": "Übernehmen und bestätigen", + "cancel": "Abbrechen", + "busy": "Wird ausgeführt …", + "validUntil": "Gültig bis {time} Uhr", + "status": { + "proposed": "Offen – bitte bestätigen", + "confirmed": "Ausgeführt", + "discarded": "Verworfen", + "expired": "Abgelaufen", + "failed": "Nicht ausgeführt" + }, + "kind": { + "transition_work_order": "Status ändern", + "book_time": "Zeit buchen", + "record_material": "Material erfassen", + "add_note": "Notiz speichern", + "suggest_report_fields": "Berichtsvorschlag" + }, + "action": { + "accept": "Auftrag {number} annehmen", + "start_travel": "Anfahrt zu {number} starten", + "start_work": "Arbeit an {number} starten", + "pause": "Pause bei {number}", + "resume": "Arbeit an {number} fortsetzen", + "complete": "Auftrag {number} technisch abschließen" + }, + "switchFrom": "Die laufende Uhr von {number} wird pausiert.", + "completeHint": "Die eigene Uhr wird beendet. Fehlende Pflichtangaben verhindern den Abschluss.", + "order": "Auftrag {number}", + "timeLine": "{date} · {from}–{to} Uhr · {duration} Std.", + "timeType": { + "work": "Arbeitszeit", + "travel": "Anfahrt", + "return_travel": "Rückfahrt", + "material_procurement": "Material holen" + }, + "type": "Art", + "date": "Datum", + "from": "Von", + "to": "Bis", + "reason": "Grund", + "note": "Notiz", + "approvalHint": "Zählt nach Freigabe durch Teamleiter oder Büro.", + "pendingApproval": "Zur Freigabe eingereicht", + "planned": "Geplante Position", + "additional": "Zusatzmaterial", + "name": "Bezeichnung", + "quantity": "Menge", + "unit": "Einheit", + "deviationReason": "Grund der Abweichung", + "noteKind": "Art der Notiz", + "text": "Text", + "reportFields": "Vorschläge für den {type}", + "reportType": { + "daily": "Tagesbericht", + "completion": "Abschlussbericht" + }, + "reportHint": "Erscheint im Bericht als „Vorschlag vom Lotsen“ und wird dort übernommen oder verworfen.", + "openReport": "Bericht öffnen", + "field": { + "workPerformed": "Ausgeführte Leistungen", + "deviations": "Abweichungen", + "additionalWork": "Zusatzarbeiten", + "openItems": "Offene Punkte", + "nextSteps": "Nächste Schritte", + "hints": "Hinweise" + } + }, + "noteKind": { + "work_done": "Erledigte Arbeit", + "deviation": "Abweichung", + "problem": "Problem", + "additional_work": "Zusatzarbeit", + "not_executable": "Nicht ausführbar", + "follow_up": "Nacharbeit", + "recommendation": "Empfehlung", + "customer_note": "Hinweis vom Kunden", + "general": "Allgemein" + }, + "system": { + "failed": "Nicht ausgeführt ({number}): {reason}", + "stopped": "„Alle bestätigen“ gestoppt – {remaining, plural, one {# Karte ist} other {# Karten sind}} noch offen.", + "rounds_exceeded": "Das war zu umfangreich. Bitte kürzer oder in Schritten schreiben.", + "turn_budget": "Das war zu umfangreich. Bitte kürzer oder in Schritten schreiben.", + "refusal": "Dazu kann der Lotse nichts vorschlagen.", + "provider_failed": "Lotse ist gerade nicht erreichbar. Bitte später erneut versuchen.", + "proposals_ready": "Vorschläge liegen bereit. Bitte prüfen und bestätigen.", + "empty": "Keine Antwort erhalten. Bitte anders formulieren." + }, + "links": { + "order": "Auftrag öffnen", + "report": "Bericht öffnen", + "myTime": "Meine Zeiten öffnen", + "materials": "Material öffnen", + "otherOrder": "Laufenden Auftrag öffnen", + "runningSession": "Uhr eines Kollegen läuft noch" + }, + "errors": { + "generic": "Das hat nicht geklappt. Bitte erneut versuchen.", + "not_found": "Nicht gefunden.", + "forbidden": "Dafür fehlt die Berechtigung.", + "disabled": "Lotse ist für diesen Betrieb ausgeschaltet.", + "chat_disabled": "Der Lotse-Chat ist für diesen Betrieb ausgeschaltet.", + "not_configured": "Lotse ist nicht eingerichtet.", + "provider_failed": "Lotse ist gerade nicht erreichbar. Bitte später erneut versuchen.", + "budget_exceeded": "Das monatliche KI-Kontingent des Betriebs ist aufgebraucht.", + "invalid": "Bitte Eingaben prüfen.", + "conflict": "Inzwischen geändert. Bitte neu laden.", + "expired": "Vorschlag abgelaufen (nach 30 Minuten). Bitte neu anfragen.", + "already_decided": "Dieser Vorschlag ist schon erledigt.", + "tampered": "Der Vorschlag wurde verändert und nicht ausgeführt.", + "blocked": "Pflichtangaben fehlen.", + "offline": "Keine Verbindung – nichts gesendet.", + "overlap": "Der Zeitraum überschneidet sich mit einer erfassten Zeit.", + "too_old": "Nachträge sind nur 7 Tage rückwirkend möglich.", + "in_future": "Zeiten in der Zukunft können nicht gebucht werden.", + "too_long": "Ein Eintrag darf höchstens 16 Stunden lang sein.", + "end_before_start": "Das Ende liegt vor dem Beginn.", + "work_order_status": "Im aktuellen Auftragsstatus nicht möglich.", + "other_session_running": "Auf einem anderen Auftrag läuft noch die Uhr.", + "transition_not_allowed": "Dieser Statuswechsel ist gerade nicht möglich.", + "transition_forbidden": "Dafür fehlt die Berechtigung.", + "not_editable": "Der Bericht ist eingereicht und kann nicht mehr geändert werden.", + "reason_required": "Der Grund fehlt.", + "use_billing_overview": "Nur über die Abrechnungsübersicht möglich." + }, + "unavailable": { + "title": "Lotse-Chat nicht verfügbar", + "back": "Zur Übersicht" } } } diff --git a/messages/en/field.json b/messages/en/field.json index 57a6825..9863338 100644 --- a/messages/en/field.json +++ b/messages/en/field.json @@ -6,7 +6,8 @@ "emergency": "Emergency", "sync": "Sync", "profile": "Profile", - "approvalsBadge": "{count} time entries to approve" + "approvalsBadge": "{count} time entries to approve", + "lotse": "Lotse" }, "connection": { "online": "Online", diff --git a/messages/en/lotse.json b/messages/en/lotse.json index 00b78a4..aa89269 100644 --- a/messages/en/lotse.json +++ b/messages/en/lotse.json @@ -89,6 +89,8 @@ "use": "Usage", "enabled": "Use Lotse in this business", "enabledHint": "Switched off: no report drafts, no summaries, voice notes without transcription.", + "chatEnabled": "Lotse chat for technicians", + "chatEnabledHint": "Technicians write or talk to the Lotse. Actions (status, times, material, notes) are only executed after the technician confirms them.", "addressForm": "Lotse form of address", "addressFormHint": "Applies consistently to all Lotse texts in this business.", "address": { @@ -114,7 +116,9 @@ "order": "Work order title, description and scope", "notes": "Activity notes and voice note texts", "work": "Checklist, material with deviation reasons, photo comments, working times", - "audio": "For transcription: the audio file of the voice note" + "audio": "For transcription: the audio file of the voice note", + "chat": "Lotse chat: the technician's messages (phone numbers, e-mail and postal addresses replaced) and the order data above. Customer names and contact data only as placeholders – values are inserted only in the answer on the technician's device.", + "chatAudio": "Voice input in the chat: the recording for transcription (not stored)" }, "notSentTitle": "Not sent", "notSent": { @@ -152,7 +156,172 @@ "voice_summary": "Voice note summary", "transcription": "Transcription", "import_extraction": "Order import", - "completeness_check": "Completeness check" + "completeness_check": "Completeness check", + "lotse_chat": "Lotse chat" + } + }, + "chat": { + "title": "Lotse chat", + "intro": "Briefly write or say what is done – e.g. “Order finished, storage tank installed, 2 hrs, 1 filter fitted”. The Lotse proposes actions. Nothing is saved before confirmation.", + "context": "Order {number} · {title}", + "noContext": "No order context", + "ask": "Ask the Lotse", + "input": "Message to the Lotse", + "send": "Send", + "sending": "Lotse is thinking …", + "newChat": "New chat", + "userLabel": "Own message", + "lotseLabel": "Lotse", + "empty": "No messages yet.", + "notConfigured": "The Lotse is not set up. The office can configure the AI provider.", + "offline": "No connection. The Lotse needs internet – your input is kept.", + "chipsLabel": "Suggested answers", + "linksLabel": "Complete directly", + "confirmAll": "Confirm all ({count})", + "mic": { + "record": "Speak", + "stop": "Stop recording", + "recording": "Recording · {time}", + "transcribing": "Converting to text …", + "unavailable": "Voice input is not set up.", + "denied": "No access to the microphone.", + "unsupported": "Voice input is not supported on this device.", + "failed": "Conversion to text failed. Please type.", + "hint": "Check the text before sending." + }, + "card": { + "confirm": "Confirm", + "edit": "Edit", + "discard": "Discard", + "saveConfirm": "Apply and confirm", + "cancel": "Cancel", + "busy": "Running …", + "validUntil": "Valid until {time}", + "status": { + "proposed": "Open – please confirm", + "confirmed": "Done", + "discarded": "Discarded", + "expired": "Expired", + "failed": "Not executed" + }, + "kind": { + "transition_work_order": "Change status", + "book_time": "Book time", + "record_material": "Record material", + "add_note": "Save note", + "suggest_report_fields": "Report suggestion" + }, + "action": { + "accept": "Accept order {number}", + "start_travel": "Start travel to {number}", + "start_work": "Start work on {number}", + "pause": "Pause {number}", + "resume": "Resume work on {number}", + "complete": "Technically complete order {number}" + }, + "switchFrom": "The running clock of {number} will be paused.", + "completeHint": "Your own clock is stopped. Missing mandatory items prevent completion.", + "order": "Order {number}", + "timeLine": "{date} · {from}–{to} · {duration} hrs", + "timeType": { + "work": "Working time", + "travel": "Travel", + "return_travel": "Return travel", + "material_procurement": "Fetching material" + }, + "type": "Type", + "date": "Date", + "from": "From", + "to": "To", + "reason": "Reason", + "note": "Note", + "approvalHint": "Counts after approval by the team lead or office.", + "pendingApproval": "Submitted for approval", + "planned": "Planned item", + "additional": "Additional material", + "name": "Name", + "quantity": "Quantity", + "unit": "Unit", + "deviationReason": "Reason for deviation", + "noteKind": "Note type", + "text": "Text", + "reportFields": "Suggestions for the {type}", + "reportType": { + "daily": "daily report", + "completion": "completion report" + }, + "reportHint": "Appears in the report as “Suggestion from the Lotse” and is accepted or discarded there.", + "openReport": "Open report", + "field": { + "workPerformed": "Work performed", + "deviations": "Deviations", + "additionalWork": "Additional work", + "openItems": "Open items", + "nextSteps": "Next steps", + "hints": "Notes" + } + }, + "noteKind": { + "work_done": "Work done", + "deviation": "Deviation", + "problem": "Problem", + "additional_work": "Additional work", + "not_executable": "Not executable", + "follow_up": "Follow-up", + "recommendation": "Recommendation", + "customer_note": "Customer note", + "general": "General" + }, + "system": { + "failed": "Not executed ({number}): {reason}", + "stopped": "“Confirm all” stopped – {remaining, plural, one {# card is} other {# cards are}} still open.", + "rounds_exceeded": "That was too much at once. Please write shorter or in steps.", + "turn_budget": "That was too much at once. Please write shorter or in steps.", + "refusal": "The Lotse cannot propose anything for this.", + "provider_failed": "The Lotse is not reachable right now. Please try again later.", + "proposals_ready": "Proposals are ready. Please check and confirm.", + "empty": "No answer received. Please rephrase." + }, + "links": { + "order": "Open order", + "report": "Open report", + "myTime": "Open my times", + "materials": "Open material", + "otherOrder": "Open running order", + "runningSession": "A colleague's clock is still running" + }, + "errors": { + "generic": "That did not work. Please try again.", + "not_found": "Not found.", + "forbidden": "You lack the permission.", + "disabled": "The Lotse is switched off for this company.", + "chat_disabled": "The Lotse chat is switched off for this company.", + "not_configured": "The Lotse is not set up.", + "provider_failed": "The Lotse is not reachable right now. Please try again later.", + "budget_exceeded": "The company's monthly AI quota is used up.", + "invalid": "Please check your input.", + "conflict": "Changed in the meantime. Please reload.", + "expired": "Proposal expired (after 30 minutes). Please ask again.", + "already_decided": "This proposal has already been handled.", + "tampered": "The proposal was changed and not executed.", + "blocked": "Mandatory items are missing.", + "offline": "No connection – nothing sent.", + "overlap": "The period overlaps with recorded time.", + "too_old": "Entries can only be added up to 7 days back.", + "in_future": "Times in the future cannot be booked.", + "too_long": "An entry may be at most 16 hours long.", + "end_before_start": "The end is before the start.", + "work_order_status": "Not possible in the current order status.", + "other_session_running": "A clock is still running on another order.", + "transition_not_allowed": "This status change is not possible right now.", + "transition_forbidden": "You lack the permission.", + "not_editable": "The report has been submitted and can no longer be changed.", + "reason_required": "The reason is missing.", + "use_billing_overview": "Only possible via the billing overview." + }, + "unavailable": { + "title": "Lotse chat not available", + "back": "Back to overview" } } } diff --git a/src/app/(app)/settings/lotse/page.tsx b/src/app/(app)/settings/lotse/page.tsx index 449a264..a74f5e9 100644 --- a/src/app/(app)/settings/lotse/page.tsx +++ b/src/app/(app)/settings/lotse/page.tsx @@ -61,6 +61,15 @@ export default async function LotseSettingsPage({ searchParams }: { searchParams {t("settings.enabledHint")} + {/* L16 Lotse-Chat für Monteure */} + +
{t("settings.addressForm")}

{t("settings.addressFormHint")}

@@ -127,7 +136,7 @@ export default async function LotseSettingsPage({ searchParams }: { searchParams

{t("settings.sentTitle")}

diff --git a/src/app/(field)/m/(core)/lotse/layout.tsx b/src/app/(field)/m/(core)/lotse/layout.tsx new file mode 100644 index 0000000..c4faabe --- /dev/null +++ b/src/app/(field)/m/(core)/lotse/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Modul-Gate „lotse" für den Lotse-Chat (L16); „field" gilt bereits über (core)/layout.tsx. */ +export default async function LotseChatLayout({ children }: Readonly<{ children: React.ReactNode }>) { + await requireModule("lotse"); + return <>{children}; +} diff --git a/src/app/(field)/m/(core)/lotse/page.tsx b/src/app/(field)/m/(core)/lotse/page.tsx new file mode 100644 index 0000000..0f67906 --- /dev/null +++ b/src/app/(field)/m/(core)/lotse/page.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getLocale, getTranslations } from "next-intl/server"; +import { ChevronLeft, CircleSlash } from "lucide-react"; +import type { ChatView } from "@/lib/lotse/chat"; +import { isAiConfigured } from "@/server/ai/client"; +import { transcriptionConfig } from "@/server/ai/transcription/openai-compatible"; +import { ServiceError } from "@/server/services/context"; +import { fieldPageContext } from "@/server/services/field/page-context"; +import { getChatView } from "@/server/services/lotse/chat/conversations"; +import { tenantTimezone } from "@/server/services/work-orders/_shared"; +import { LotseMark } from "@/components/lotse/lotse-mark"; +import { LotseChat } from "@/components/lotse/chat/lotse-chat"; +import { card } from "@/components/field/ui"; + +/** + * `/m/lotse` — Lotse chat for technicians (lane L16). `?order=` opens the chat with order context + * (button „Lotse fragen" in the order detail). Nothing is executed without confirmation. + */ +export default async function LotseChatPage({ searchParams }: { searchParams: Promise<{ order?: string }> }) { + const [ctx, sp, t, locale] = await Promise.all([fieldPageContext(), searchParams, getTranslations("lotse"), getLocale()]); + + let view: ChatView; + try { + view = await getChatView(ctx, { workOrderId: sp.order?.trim() || null }); + } catch (err) { + if (!(err instanceof ServiceError)) throw err; + if (err.code === "not_found") notFound(); + const reason = (err.details as { reason?: string } | undefined)?.reason; + const code = reason === "disabled" || reason === "chat_disabled" ? reason : err.code === "forbidden" ? "forbidden" : "generic"; + return ( +
+
+

+ + {t("chat.unavailable.title")} +

+

{t(`chat.errors.${code}`)}

+ + {t("chat.unavailable.back")} + +
+
+ ); + } + + const timeZone = await tenantTimezone(ctx); + return ( +
+ {view.workOrder && ( + + + {t("chat.context", { number: view.workOrder.number, title: view.workOrder.title })} + + )} +

+ +

+ +
+ ); +} diff --git a/src/app/(field)/m/(core)/orders/[id]/page.tsx b/src/app/(field)/m/(core)/orders/[id]/page.tsx index de41be1..9f5a1fa 100644 --- a/src/app/(field)/m/(core)/orders/[id]/page.tsx +++ b/src/app/(field)/m/(core)/orders/[id]/page.tsx @@ -33,6 +33,7 @@ import { card, toneClasses } from "@/components/field/ui"; import { loadOrder } from "./load"; import { LotseCompletenessCard } from "@/components/lotse/completeness-card"; import { MilestonesSection } from "@/components/field/milestones"; +import { LotseAskButton } from "@/components/lotse/chat/ask-button"; function Section({ title, icon: Icon, children, href, summary }: { title: string; icon: LucideIcon; children?: React.ReactNode; href?: string; summary?: string }) { const head = ( @@ -168,6 +169,7 @@ export default async function OrderDetailPage({ params }: { params: Promise<{ id )} {editable && } + {editable && }{/* L16 Lotse-Chat */} {/* L14 Abrechnungsübersicht */} diff --git a/src/app/(field)/m/layout.tsx b/src/app/(field)/m/layout.tsx index ca6f601..170ff9e 100644 --- a/src/app/(field)/m/layout.tsx +++ b/src/app/(field)/m/layout.tsx @@ -5,6 +5,7 @@ import { can } from "@/server/services/context"; import { fieldPageContext } from "@/server/services/field/page-context"; import { getMyActiveSession } from "@/server/services/field/sessions"; import { countPendingTimeEntries } from "@/server/services/field/time-entries"; +import { canUseLotseChat } from "@/server/services/lotse/chat/access"; import { cn } from "@/lib/utils"; import { CraftviaLogo } from "@/components/brand/craftvia-logo"; import { AccountInactiveNotice } from "@/components/account-inactive-notice"; @@ -14,19 +15,20 @@ import { RunningClockBar, type ClockSession } from "@/components/field/running-c import { OfflineRuntime } from "@/components/offline/offline-runtime"; /** L12: own running/paused session + open approvals for the shell (never blocks the page). */ -async function shellTimeState(): Promise<{ clock: ClockSession | null; approvals: number }> { +async function shellTimeState(): Promise<{ clock: ClockSession | null; approvals: number; lotseChat: boolean }> { try { const ctx = await fieldPageContext(); - const [session, approvals] = await Promise.all([can(ctx, "field:execute") ? getMyActiveSession(ctx) : Promise.resolve(null), countPendingTimeEntries(ctx)]); + const [session, approvals, lotseChat] = await Promise.all([can(ctx, "field:execute") ? getMyActiveSession(ctx) : Promise.resolve(null), countPendingTimeEntries(ctx), canUseLotseChat(ctx)]); return { clock: session ? { status: session.status, workOrderId: session.workOrderId, number: session.number, title: session.title, segmentType: session.segmentType, segmentStartedAt: session.segmentStartedAt, closedSeconds: session.closedSeconds } : null, approvals, + lotseChat, // L16 }; } catch { // module "field" disabled or no field permissions: shell without clock/badge - return { clock: null, approvals: 0 }; + return { clock: null, approvals: 0, lotseChat: false }; } } @@ -55,7 +57,7 @@ export default async function FieldShell({ children }: Readonly<{ children: Reac
{children}
- +
); } diff --git a/src/components/audit-trail.tsx b/src/components/audit-trail.tsx index c1c0623..1a88009 100644 --- a/src/components/audit-trail.tsx +++ b/src/components/audit-trail.tsx @@ -88,6 +88,7 @@ const ENTITY_LABEL: Record = { number_sequence: "Nummernkreis", ai_generation: "Lotse (KI)", lotse_settings: "Lotse-Einstellungen", + lotse_chat_message: "Lotse-Chat", lotse_action_proposal: "Lotse-Aktionskarte", lotse_chat_retention: "Lotse-Chat (Aufbewahrung)", // L16 }; const fmt = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short" }); diff --git a/src/components/field/bottom-nav.tsx b/src/components/field/bottom-nav.tsx index da311fc..bdd31a1 100644 --- a/src/components/field/bottom-nav.tsx +++ b/src/components/field/bottom-nav.tsx @@ -3,12 +3,13 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useTranslations } from "next-intl"; -import { ClipboardList, House, RefreshCw, Siren, User } from "lucide-react"; +import { ClipboardList, Compass, House, RefreshCw, Siren, User } from "lucide-react"; import { cn } from "@/lib/utils"; const ITEMS = [ { href: "/m", key: "today", icon: House, exact: true }, { href: "/m/orders", key: "orders", icon: ClipboardList, exact: false }, + { href: "/m/lotse", key: "lotse", icon: Compass, exact: false }, // L16 Lotse-Chat (only when usable) { href: "/m/emergency", key: "emergency", icon: Siren, exact: false }, { href: "/m/sync", key: "sync", icon: RefreshCw, exact: false }, { href: "/m/profile", key: "profile", icon: User, exact: false }, @@ -18,13 +19,13 @@ const ITEMS = [ * Bottom navigation of the mobile shell (Spec §22): Heute · Aufträge · Notdienst · Sync · Profil. * L12: badge counter of open time approvals on „Profil" (number + accessible label). */ -export function BottomNav({ approvals = 0 }: { approvals?: number }) { +export function BottomNav({ approvals = 0, lotse = false }: { approvals?: number; lotse?: boolean }) { const t = useTranslations("field.nav"); const pathname = usePathname(); return (