diff --git a/messages/de/dashboard.json b/messages/de/dashboard.json index 6d8db7b..33e7ba1 100644 --- a/messages/de/dashboard.json +++ b/messages/de/dashboard.json @@ -2,7 +2,34 @@ "title": "Dashboard", "crumb": "Übersicht", "subtitle": "Willkommen, {name} — {tenant}", - "placeholderTitle": "Dashboard in Umsetzung", - "placeholder": "Hier erscheinen künftig offene Aufträge, heutige Einsätze, freizugebende Berichte und Notdienst-Meldungen.", - "moduleDisabled": "Das angeforderte Modul ist für Ihren Betrieb nicht freigeschaltet." + "moduleDisabled": "Das angeforderte Modul ist für Ihren Betrieb nicht freigeschaltet.", + "workOrdersDisabled": "Das Modul Aufträge ist nicht freigeschaltet – es gibt keine Kennzahlen.", + "filterTitle": "Kennzahlen eingrenzen", + "tiles": { + "open": "Offene Aufträge", + "today": "Heutige Einsätze", + "running": "Laufende Einsätze", + "not_accepted": "Nicht angenommen", + "overdue": "Überfällig", + "reports_in_review": "Berichte zur Prüfung", + "completed": "Abgeschlossen", + "billing": "Zur Abrechnung", + "emergency_new": "Neue Notdienste", + "missing_signatures": "Fehlende Unterschriften", + "sync_conflicts": "Sync-Konflikte" + }, + "hints": { + "open": "Noch nicht abgerechnet oder storniert", + "today": "Für heute geplant", + "running": "Unterwegs, in Arbeit, pausiert", + "not_accepted": "Zugewiesen, noch nicht angenommen", + "overdue": "Geplantes Ende überschritten", + "reports_in_review": "Eingereicht, noch nicht freigegeben", + "completed": "Technisch abgeschlossen oder weiter", + "billing": "Bereit zur Abrechnung", + "emergency_new": "Neu oder zur Prüfung", + "missing_signatures": "Unterschrift ausstehend", + "sync_conflicts": "Offline-Änderungen prüfen" + }, + "open": "Anzeigen" } diff --git a/messages/de/search.json b/messages/de/search.json new file mode 100644 index 0000000..8874899 --- /dev/null +++ b/messages/de/search.json @@ -0,0 +1,28 @@ +{ + "crumb": "Suche", + "title": "Suche", + "placeholder": "Aufträge, Kunden, Objekte, Adressen, Dokumente, Notizen", + "submit": "Suchen", + "hint": "Mindestens 2 Zeichen eingeben.", + "noResults": "Keine Treffer für „{q}“.", + "resultsFor": "Treffer für „{q}“", + "filters": { + "from": "Von", + "to": "Bis", + "status": "Status", + "team": "Team", + "any": "Alle", + "apply": "Filtern", + "reset": "Filter zurücksetzen", + "note": "Zeitraum, Status und Team grenzen Aufträge und alles am Auftrag ein." + }, + "sections": { + "workOrders": "Aufträge", + "customers": "Kunden", + "sites": "Objekte", + "contacts": "Ansprechpartner", + "documents": "Dokumente", + "notes": "Notizen" + }, + "maxHint": "Es werden höchstens 20 Treffer je Bereich angezeigt." +} diff --git a/messages/de/settingsTemplates.json b/messages/de/settingsTemplates.json new file mode 100644 index 0000000..d84479c --- /dev/null +++ b/messages/de/settingsTemplates.json @@ -0,0 +1,73 @@ +{ + "crumb": "Einstellungen", + "nav": { + "orderTypes": "Auftragsarten", + "checklists": "Checklisten-Vorlagen", + "numbering": "Nummernkreise" + }, + "orderTypes": { + "title": "Auftragsarten", + "sub": "Auftragsarten je Betrieb – mit Vorgabe, ob eine Kundenunterschrift nötig ist", + "key": "Schlüssel", + "name": "Bezeichnung", + "signatureRequired": "Unterschrift erforderlich", + "active": "Aktiv", + "sortOrder": "Reihenfolge", + "create": "Auftragsart anlegen", + "save": "Speichern", + "keyHint": "Kleinbuchstaben, Ziffern, Unterstrich – nicht änderbar", + "inactive": "Inaktiv", + "yes": "Ja", + "no": "Nein" + }, + "checklists": { + "title": "Checklisten-Vorlagen", + "sub": "Vorlagen je Auftragsart – werden beim Anlegen eines Auftrags übernommen", + "empty": "Noch keine Vorlagen.", + "new": "Neue Vorlage", + "edit": "Bearbeiten", + "name": "Name", + "orderType": "Auftragsart", + "noOrderType": "— keine —", + "active": "Aktiv", + "items": "Checklistenpunkte", + "itemsHint": "Eine Zeile je Punkt. Zusätze: „*“ am Anfang = Pflicht, „[Foto]“ am Ende = mit Foto.", + "photos": "Pflichtfotos", + "photosHint": "Eine Zeile je Pflichtfoto.", + "suggestion": "Standardvorschlag einfügen", + "suggestionHint": "Vorschlag aus den Standardpunkten; vor dem Speichern anpassen.", + "save": "Vorlage speichern", + "itemCount": "{count, plural, =0 {Keine Punkte} one {# Punkt} other {# Punkte}}", + "photoCount": "{count, plural, =0 {keine Pflichtfotos} one {# Pflichtfoto} other {# Pflichtfotos}}", + "inactive": "Inaktiv", + "photoSuffix": "[Foto]" + }, + "numbering": { + "title": "Nummernkreise", + "sub": "Präfix und Stellenzahl der fortlaufenden Nummern. Der Zähler läuft immer weiter.", + "key": "Nummernkreis", + "prefix": "Präfix", + "padding": "Stellen", + "next": "Nächste Nummer", + "save": "Speichern", + "keys": { + "work_order": "Aufträge", + "emergency": "Notdienste", + "customer": "Kunden", + "report": "Berichte" + } + }, + "saved": "Gespeichert.", + "errors": { + "forbidden": "Dafür fehlt die Berechtigung.", + "invalid": "Eingaben prüfen.", + "not_found": "Nicht gefunden.", + "internal": "Vorgang nicht möglich.", + "validation_failed": "Pflichtfelder prüfen.", + "order_type_key_taken": "Dieser Schlüssel ist bereits vergeben.", + "order_type_not_found": "Auftragsart nicht gefunden.", + "template_not_found": "Vorlage nicht gefunden.", + "duplicate_item_key": "Zwei Checklistenpunkte haben dieselbe Bezeichnung.", + "duplicate_photo_key": "Zwei Pflichtfotos haben dieselbe Bezeichnung." + } +} diff --git a/messages/de/workOrders.json b/messages/de/workOrders.json new file mode 100644 index 0000000..d25a8c0 --- /dev/null +++ b/messages/de/workOrders.json @@ -0,0 +1,445 @@ +{ + "crumb": "Aufträge", + "title": "Aufträge", + "subtitle": "Planen, zuweisen, prüfen und zur Abrechnung freigeben", + "new": "Neuer Auftrag", + "conflictsLink": "Sync-Konflikte", + "allGroups": "Alle", + "empty": "Keine Aufträge für diese Auswahl.", + "resultCount": "{count, plural, =0 {Keine Aufträge} one {# Auftrag} other {# Aufträge}}", + "presetActive": "Kachel-Filter: {name}", + "removePreset": "Kachel-Filter entfernen", + "view": { "cards": "Karten", "table": "Tabelle" }, + "pagination": { "prev": "Zurück", "next": "Weiter", "page": "Seite {page} von {pages}" }, + "filter": { + "title": "Filter", + "q": "Nummer, Titel, Kunde, Ort", + "from": "Von", + "to": "Bis", + "customer": "Kunde", + "site": "Objekt", + "team": "Team", + "user": "Monteur", + "status": "Status", + "orderType": "Auftragsart", + "priority": "Priorität", + "sort": "Sortierung", + "any": "Alle", + "apply": "Filtern", + "reset": "Zurücksetzen" + }, + "sort": { + "plannedStart": "Geplanter Beginn", + "createdAt": "Angelegt", + "updatedAt": "Zuletzt geändert", + "number": "Nummer", + "priority": "Priorität", + "status": "Status", + "asc": "aufsteigend", + "desc": "absteigend" + }, + "columns": { + "number": "Nummer", + "title": "Titel", + "customer": "Kunde", + "site": "Objekt", + "planned": "Geplant", + "team": "Team", + "status": "Status", + "priority": "Priorität" + }, + "status": { + "draft": "Entwurf", + "review_required": "Prüfung erforderlich", + "planned": "Geplant", + "assigned": "Zugewiesen", + "accepted": "Angenommen", + "en_route": "In Anfahrt", + "in_progress": "In Bearbeitung", + "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": "Zur Abrechnung freigegeben", + "billed": "Abgerechnet", + "cancelled": "Storniert" + }, + "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" + }, + "priority": { "low": "Niedrig", "normal": "Normal", "high": "Hoch", "urgent": "Dringend" }, + "billingType": { + "fixed": "Pauschal", + "time_material": "Nach Aufwand", + "maintenance_contract": "Wartungsvertrag", + "warranty": "Gewährleistung" + }, + "emergency": "Notdienst", + "unassigned": "Nicht zugewiesen", + "noDate": "Ohne Termin", + "create": { + "title": "Neuer Auftrag", + "sub": "Kunde wählen, Auftrag erfassen", + "stepCustomer": "1. Kunde suchen", + "customerSearch": "Firma, Name, Kundennummer oder Ort", + "searchButton": "Suchen", + "noCustomers": "Kein Kunde gefunden. Kunden werden unter „Kunden“ angelegt.", + "chooseCustomer": "Auswählen", + "changeCustomer": "Anderen Kunden wählen", + "stepDetails": "2. Auftrag erfassen", + "submit": "Auftrag anlegen", + "pending": "Lege an…" + }, + "fields": { + "title": "Titel", + "customer": "Kunde", + "site": "Objekt", + "contact": "Ansprechpartner", + "none": "— keine Auswahl —", + "orderType": "Auftragsart", + "priority": "Priorität", + "plannedStart": "Geplanter Beginn", + "plannedEnd": "Geplantes Ende", + "description": "Beschreibung", + "scope": "Leistungsumfang", + "internalNotes": "Hinweise intern (nur Büro)", + "technicianNotes": "Hinweise für Monteure", + "signatureRequired": "Unterschrift erforderlich", + "signatureFromType": "Wie Auftragsart", + "yes": "Ja", + "no": "Nein", + "billingType": "Abrechnungsart", + "externalOrderNumber": "Externe Auftragsnummer", + "offerNumber": "Angebotsnummer", + "status": "Anlegen als", + "timezoneHint": "Zeiten in {tz}" + }, + "detail": { + "back": "Zurück zu Aufträgen", + "version": "Version {version}", + "nextStep": "Nächster Schritt", + "moreActions": "Weitere Statusänderungen", + "edit": "Bearbeiten", + "assign": "Zuweisen", + "reassign": "Zuweisung ändern", + "cancel": "Stornieren", + "noActions": "Für Sie ist gerade keine Statusänderung möglich.", + "blockersTitle": "Vor dem Abschluss fehlt noch:", + "tabs": { + "overview": "Übersicht", + "checklist": "Checkliste & Pflichtfotos", + "material": "Material", + "times": "Zeiten", + "photos": "Fotos", + "notes": "Notizen", + "reports": "Berichte", + "documents": "Dokumente", + "history": "Verlauf" + } + }, + "overview": { + "customer": "Kunde", + "site": "Objekt", + "contact": "Ansprechpartner", + "planning": "Planung", + "assignment": "Zuweisung", + "team": "Team", + "teamLead": "Teamleiter", + "assignees": "Monteure", + "order": "Auftrag", + "created": "Angelegt", + "updated": "Geändert", + "accessNotes": "Zugang", + "safetyNotes": "Sicherheit" + }, + "checklist": { + "items": "Checkliste", + "photos": "Pflichtfotos", + "empty": "Keine Checklistenpunkte.", + "emptyPhotos": "Keine Pflichtfotos.", + "required": "Pflicht", + "optional": "Optional", + "requiresPhoto": "mit Foto", + "done": "Erledigt", + "open": "Offen", + "photoCount": "{count, plural, =0 {Kein Foto} one {# Foto} other {# Fotos}}", + "addItem": "Punkt hinzufügen", + "addPhoto": "Pflichtfoto hinzufügen", + "label": "Bezeichnung", + "applyTemplate": "Vorlage übernehmen", + "template": "Vorlage", + "applyButton": "Übernehmen", + "remove": "Entfernen", + "complete": "Alle Abschluss-Voraussetzungen erfüllt." + }, + "material": { + "title": "Material – geplant und tatsächlich", + "empty": "Kein Material geplant oder erfasst.", + "name": "Bezeichnung", + "articleNumber": "Artikelnummer", + "planned": "Geplant", + "actual": "Tatsächlich", + "deviation": "Abweichung", + "unit": "Einheit", + "notes": "Bemerkung", + "reason": "Begründung", + "additional": "Zusätzlich", + "notRecorded": "Noch nicht erfasst", + "add": "Material planen", + "remove": "Entfernen", + "usage": { + "fully_used": "Vollständig verwendet", + "partially_used": "Teilweise verwendet", + "not_used": "Nicht verwendet", + "additional": "Zusätzlich verwendet" + } + }, + "times": { + "empty": "Noch keine Zeiten erfasst.", + "person": "Person", + "start": "Beginn", + "end": "Ende", + "duration": "Arbeitszeit", + "minutes": "{minutes} min", + "corrected": "Korrigiert: {reason}", + "sessionStatus": { + "en_route": "Unterwegs", + "running": "Läuft", + "paused": "Pausiert", + "ended": "Beendet" + }, + "entryType": { + "travel": "Anfahrt", + "work": "Arbeit", + "break": "Pause", + "material_procurement": "Materialbeschaffung", + "return_travel": "Rückfahrt", + "interruption": "Unterbrechung" + } + }, + "photos": { + "empty": "Noch keine Fotos.", + "phase": { "before": "Vorher", "during": "Während", "after": "Nachher" }, + "open": "Öffnen", + "inReport": "Im Bericht" + }, + "notes": { + "empty": "Keine Notizen.", + "internal": "Hinweise intern", + "technician": "Hinweise für Monteure", + "kind": { + "work_done": "Ausgeführt", + "deviation": "Abweichung", + "problem": "Problem", + "additional_work": "Zusatzarbeit", + "not_executable": "Nicht ausführbar", + "follow_up": "Folgearbeit", + "recommendation": "Empfehlung", + "customer_note": "Kundenhinweis", + "general": "Allgemein" + } + }, + "reports": { + "empty": "Noch keine Berichte.", + "type": { "daily": "Tagesbericht", "completion": "Abschlussbericht" }, + "status": { + "draft": "Entwurf", + "submitted": "Zur Prüfung", + "team_approved": "Vom Teamleiter freigegeben", + "approved": "Freigegeben", + "rejected": "Zurückgewiesen", + "superseded": "Ersetzt" + }, + "open": "Bericht öffnen", + "signature": "Unterschrift: {outcome}", + "signatureOutcome": { + "signed": "unterschrieben", + "customer_absent": "Kunde nicht anwesend", + "refused": "verweigert", + "later": "später", + "not_required": "nicht erforderlich" + }, + "billingTitle": "Abrechnung", + "billingHint": "Freigabe nur mit freigegebenem Abschlussbericht.", + "release": "Zur Abrechnung freigeben", + "reject": "Zur Korrektur zurückweisen", + "markBilled": "Als abgerechnet markieren", + "revoke": "Freigabe zurücknehmen" + }, + "documents": { + "empty": "Keine Dokumente am Auftrag.", + "upload": "Dokument hochladen", + "file": "Datei (PDF, JPG, PNG, WebP)", + "title": "Titel (optional)", + "category": "Kategorie", + "visibility": "Sichtbarkeit", + "submit": "Hochladen", + "archive": "Entfernen", + "download": "Herunterladen", + "stubHint": "Dateien werden bis zur Anbindung der Dokumentenablage direkt im Objektspeicher abgelegt.", + "categories": { + "order_confirmation": "Auftragsbestätigung", + "technical_drawing": "Technische Zeichnung", + "floor_plan": "Grundriss", + "wiring_diagram": "Schaltplan", + "assembly_instructions": "Montageanleitung", + "safety_document": "Sicherheitsdokument", + "product_document": "Produktunterlage", + "customer_note": "Kundennotiz", + "work_record": "Arbeitsnachweis", + "daily_report": "Tagesbericht", + "completion_report": "Abschlussbericht", + "customer_approval": "Kundenfreigabe", + "photo": "Foto", + "voice_note": "Sprachnotiz", + "signature": "Unterschrift", + "other": "Sonstiges" + }, + "visibilities": { + "backoffice_only": "Nur Büro", + "team_lead": "Teamleiter", + "team": "Team", + "customer_report": "Kundenbericht" + } + }, + "history": { + "statusChanges": "Statusänderungen", + "audit": "Protokoll", + "empty": "Noch keine Einträge.", + "created": "Angelegt als {status}", + "change": "{from} → {to}", + "by": "von {name}", + "system": "System", + "action": { "create": "Erstellt", "update": "Geändert", "delete": "Gelöscht" } + }, + "assign": { + "title": "Auftrag zuweisen", + "sub": "Team wählen, optional einzelne Monteure und Teamleiter", + "team": "Team", + "chooseTeam": "— Team wählen —", + "members": "Einzelne Monteure (optional)", + "teamLead": "Zuständiger Teamleiter", + "teamLeadDefault": "Teamleiter des Teams", + "noTeams": "Keine aktiven Teams. Teams werden unter „Teams“ angelegt.", + "submit": "Zuweisen", + "pending": "Weise zu…" + }, + "transition": { + "to": { + "draft": "Als Entwurf", + "review_required": "Zur Prüfung markieren", + "planned": "Einplanen", + "assigned": "Zuweisen", + "accepted": "Annehmen", + "en_route": "Anfahrt starten", + "in_progress": "Arbeit aufnehmen", + "paused": "Pausieren", + "waiting_material": "Wartet auf Material", + "daily_report_created": "Tagesbericht erstellt", + "technically_completed": "Technisch abschließen", + "signature_pending": "Unterschrift ausstehend", + "in_review": "Zur Prüfung geben", + "released_for_billing": "Zur Abrechnung freigeben", + "billed": "Als abgerechnet markieren", + "cancelled": "Stornieren" + }, + "correction": "Zur Korrektur zurückweisen", + "revoke": "Freigabe zurücknehmen", + "reasonTitle": "Begründung erforderlich", + "reason": "Begründung", + "reasonPlaceholder": "Kurz beschreiben, warum", + "confirm": "Bestätigen", + "pending": "Speichere…", + "done": "Status geändert." + }, + "edit": { + "title": "Auftrag bearbeiten", + "submit": "Speichern", + "pending": "Speichere…", + "saved": "Gespeichert." + }, + "conflicts": { + "crumb": "Aufträge", + "title": "Sync-Konflikte", + "sub": "Offline erfasste Änderungen, die nicht automatisch übernommen werden konnten", + "empty": "Keine offenen Konflikte.", + "received": "Eingegangen", + "user": "Gerät / Person", + "operation": "Vorgang", + "entity": "Auftrag", + "baseVersion": "Stand Gerät: Version {base} · aktuell: Version {current}", + "payload": "Inhalt", + "apply": "Übernehmen", + "discard": "Verwerfen", + "applied": "Übernommen.", + "discarded": "Verworfen.", + "applyHint": "Übernehmen wendet den Vorgang erneut auf den aktuellen Stand an – im Namen der Person, die ihn erfasst hat.", + "stubHint": "Übernehmen ist bis zur Anbindung der Synchronisation nur für Statusänderungen möglich." + }, + "errors": { + "not_found": "Nicht gefunden oder keine Berechtigung.", + "forbidden": "Dafür fehlt die Berechtigung.", + "invalid": "Eingaben prüfen.", + "conflict": "Der Auftrag wurde inzwischen geändert. Seite neu laden und erneut versuchen.", + "blocked": "Noch nicht möglich – es fehlen Angaben.", + "internal": "Vorgang nicht möglich.", + "validation_failed": "Pflichtfelder prüfen.", + "work_order_not_found": "Auftrag nicht gefunden.", + "customer_not_found": "Kunde nicht gefunden.", + "site_not_found": "Objekt nicht gefunden.", + "contact_not_found": "Ansprechpartner nicht gefunden.", + "site_customer_mismatch": "Das Objekt gehört zu einem anderen Kunden.", + "contact_customer_mismatch": "Der Ansprechpartner gehört zu einem anderen Kunden.", + "order_type_not_found": "Auftragsart nicht gefunden.", + "planned_end_before_start": "Das Ende liegt vor dem Beginn.", + "nothing_to_update": "Keine Änderungen.", + "not_editable": "Der Auftrag kann in diesem Status nicht mehr geändert werden.", + "delete_only_draft": "Nur Entwürfe können gelöscht werden.", + "transition_not_allowed": "Dieser Statuswechsel ist nicht vorgesehen.", + "transition_forbidden": "Diesen Statuswechsel dürfen Sie nicht ausführen.", + "transition_blocked": "Noch nicht möglich – es fehlen Angaben.", + "reason_required": "Bitte eine Begründung angeben.", + "version_conflict": "Der Auftrag wurde inzwischen geändert. Seite neu laden und erneut versuchen.", + "team_not_found": "Team nicht gefunden oder nicht aktiv.", + "user_not_found": "Mindestens eine Person ist nicht aktiv.", + "material_plan_not_found": "Materialposition nicht gefunden.", + "material_in_use": "Zu dieser Position wurde bereits Material erfasst.", + "checklist_item_not_found": "Checklistenpunkt nicht gefunden.", + "checklist_item_checked": "Erledigte Punkte bleiben als Nachweis erhalten.", + "photo_requirement_not_found": "Pflichtfoto nicht gefunden.", + "photo_requirement_has_photos": "Zu diesem Pflichtfoto gibt es bereits Fotos.", + "template_not_found": "Vorlage nicht gefunden.", + "sync_conflict_not_found": "Konflikt nicht gefunden oder bereits erledigt.", + "sync_conflict_already_resolved": "Konflikt wurde bereits erledigt.", + "sync_payload_invalid": "Der Vorgang ist unvollständig.", + "sync_user_inactive": "Die Person ist nicht mehr aktiv.", + "reapply_unsupported": "Dieser Vorgang kann noch nicht übernommen werden.", + "document_not_found": "Dokument nicht gefunden.", + "file_type_not_allowed": "Dateityp nicht erlaubt.", + "file_too_large": "Datei ist leer oder zu groß.", + "file_missing": "Bitte eine Datei wählen.", + "category_invalid": "Kategorie nicht erlaubt.", + "visibility_forbidden": "Diese Sichtbarkeit dürfen Sie nicht vergeben.", + "initial_status_not_allowed": "Dieser Anfangsstatus ist nicht erlaubt." + }, + "blockers": { + "checklist_item": "Checkliste: {label}", + "photo_requirement": "Pflichtfoto: {label}", + "running_session": "Ein Einsatz läuft noch", + "field": { + "signature": "Unterschrift fehlt", + "approved_completion_report": "Abschlussbericht ist noch nicht freigegeben" + } + }, + "saved": "Gespeichert." +} diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 01f1e6c..eee1199 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -2,7 +2,34 @@ "title": "Dashboard", "crumb": "Overview", "subtitle": "Welcome, {name} — {tenant}", - "placeholderTitle": "Dashboard under construction", - "placeholder": "Open work orders, today's jobs, reports awaiting approval and emergency calls will appear here.", - "moduleDisabled": "The requested module is not enabled for your company." + "moduleDisabled": "The requested module is not enabled for your company.", + "workOrdersDisabled": "The work orders module is not enabled – there are no figures.", + "filterTitle": "Narrow down figures", + "tiles": { + "open": "Open work orders", + "today": "Today's jobs", + "running": "Running jobs", + "not_accepted": "Not accepted", + "overdue": "Overdue", + "reports_in_review": "Reports to review", + "completed": "Completed", + "billing": "Ready for billing", + "emergency_new": "New emergencies", + "missing_signatures": "Missing signatures", + "sync_conflicts": "Sync conflicts" + }, + "hints": { + "open": "Not yet billed or cancelled", + "today": "Planned for today", + "running": "En route, in progress, paused", + "not_accepted": "Assigned, not yet accepted", + "overdue": "Planned end exceeded", + "reports_in_review": "Submitted, not yet approved", + "completed": "Technically completed or beyond", + "billing": "Ready to invoice", + "emergency_new": "New or in review", + "missing_signatures": "Signature pending", + "sync_conflicts": "Review offline changes" + }, + "open": "Show" } diff --git a/messages/en/search.json b/messages/en/search.json new file mode 100644 index 0000000..548274e --- /dev/null +++ b/messages/en/search.json @@ -0,0 +1,28 @@ +{ + "crumb": "Search", + "title": "Search", + "placeholder": "Work orders, customers, sites, addresses, documents, notes", + "submit": "Search", + "hint": "Enter at least 2 characters.", + "noResults": "No results for “{q}”.", + "resultsFor": "Results for “{q}”", + "filters": { + "from": "From", + "to": "To", + "status": "Status", + "team": "Team", + "any": "All", + "apply": "Apply", + "reset": "Reset filters", + "note": "Period, status and team narrow work orders and everything attached to them." + }, + "sections": { + "workOrders": "Work orders", + "customers": "Customers", + "sites": "Sites", + "contacts": "Contact persons", + "documents": "Documents", + "notes": "Notes" + }, + "maxHint": "At most 20 results per section are shown." +} diff --git a/messages/en/settingsTemplates.json b/messages/en/settingsTemplates.json new file mode 100644 index 0000000..a9daf08 --- /dev/null +++ b/messages/en/settingsTemplates.json @@ -0,0 +1,73 @@ +{ + "crumb": "Settings", + "nav": { + "orderTypes": "Order types", + "checklists": "Checklist templates", + "numbering": "Number sequences" + }, + "orderTypes": { + "title": "Order types", + "sub": "Order types per company – including whether a customer signature is required", + "key": "Key", + "name": "Name", + "signatureRequired": "Signature required", + "active": "Active", + "sortOrder": "Order", + "create": "Create order type", + "save": "Save", + "keyHint": "Lower-case letters, digits, underscore – cannot be changed", + "inactive": "Inactive", + "yes": "Yes", + "no": "No" + }, + "checklists": { + "title": "Checklist templates", + "sub": "Templates per order type – applied when a work order is created", + "empty": "No templates yet.", + "new": "New template", + "edit": "Edit", + "name": "Name", + "orderType": "Order type", + "noOrderType": "— none —", + "active": "Active", + "items": "Checklist items", + "itemsHint": "One line per item. Prefix “*” = required, suffix “[Foto]” = with photo.", + "photos": "Required photos", + "photosHint": "One line per required photo.", + "suggestion": "Insert default suggestion", + "suggestionHint": "Suggestion from the default items; adjust before saving.", + "save": "Save template", + "itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}", + "photoCount": "{count, plural, =0 {no required photos} one {# required photo} other {# required photos}}", + "inactive": "Inactive", + "photoSuffix": "[Foto]" + }, + "numbering": { + "title": "Number sequences", + "sub": "Prefix and number of digits of running numbers. The counter always continues.", + "key": "Sequence", + "prefix": "Prefix", + "padding": "Digits", + "next": "Next number", + "save": "Save", + "keys": { + "work_order": "Work orders", + "emergency": "Emergencies", + "customer": "Customers", + "report": "Reports" + } + }, + "saved": "Saved.", + "errors": { + "forbidden": "You lack the permission for this.", + "invalid": "Please check your input.", + "not_found": "Not found.", + "internal": "Operation not possible.", + "validation_failed": "Please check required fields.", + "order_type_key_taken": "This key is already taken.", + "order_type_not_found": "Order type not found.", + "template_not_found": "Template not found.", + "duplicate_item_key": "Two checklist items have the same label.", + "duplicate_photo_key": "Two required photos have the same label." + } +} diff --git a/messages/en/workOrders.json b/messages/en/workOrders.json new file mode 100644 index 0000000..bdeed36 --- /dev/null +++ b/messages/en/workOrders.json @@ -0,0 +1,445 @@ +{ + "crumb": "Work orders", + "title": "Work orders", + "subtitle": "Plan, assign, review and release for billing", + "new": "New work order", + "conflictsLink": "Sync conflicts", + "allGroups": "All", + "empty": "No work orders for this selection.", + "resultCount": "{count, plural, =0 {No work orders} one {# work order} other {# work orders}}", + "presetActive": "Tile filter: {name}", + "removePreset": "Remove tile filter", + "view": { "cards": "Cards", "table": "Table" }, + "pagination": { "prev": "Previous", "next": "Next", "page": "Page {page} of {pages}" }, + "filter": { + "title": "Filter", + "q": "Number, title, customer, city", + "from": "From", + "to": "To", + "customer": "Customer", + "site": "Site", + "team": "Team", + "user": "Technician", + "status": "Status", + "orderType": "Order type", + "priority": "Priority", + "sort": "Sort", + "any": "All", + "apply": "Apply", + "reset": "Reset" + }, + "sort": { + "plannedStart": "Planned start", + "createdAt": "Created", + "updatedAt": "Last changed", + "number": "Number", + "priority": "Priority", + "status": "Status", + "asc": "ascending", + "desc": "descending" + }, + "columns": { + "number": "Number", + "title": "Title", + "customer": "Customer", + "site": "Site", + "planned": "Planned", + "team": "Team", + "status": "Status", + "priority": "Priority" + }, + "status": { + "draft": "Draft", + "review_required": "Review required", + "planned": "Planned", + "assigned": "Assigned", + "accepted": "Accepted", + "en_route": "En route", + "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": "Released for billing", + "billed": "Billed", + "cancelled": "Cancelled" + }, + "statusGroup": { + "new": "New", + "planned": "Planned", + "en_route": "En route", + "in_progress": "In progress", + "documentation_incomplete": "Documentation incomplete", + "in_review": "In review", + "ready_for_billing": "Ready for billing", + "billed": "Billed", + "cancelled": "Cancelled" + }, + "priority": { "low": "Low", "normal": "Normal", "high": "High", "urgent": "Urgent" }, + "billingType": { + "fixed": "Fixed price", + "time_material": "Time and material", + "maintenance_contract": "Maintenance contract", + "warranty": "Warranty" + }, + "emergency": "Emergency", + "unassigned": "Not assigned", + "noDate": "No date", + "create": { + "title": "New work order", + "sub": "Choose a customer, enter the order", + "stepCustomer": "1. Find customer", + "customerSearch": "Company, name, customer number or city", + "searchButton": "Search", + "noCustomers": "No customer found. Customers are created under “Customers”.", + "chooseCustomer": "Select", + "changeCustomer": "Choose another customer", + "stepDetails": "2. Enter order", + "submit": "Create work order", + "pending": "Creating…" + }, + "fields": { + "title": "Title", + "customer": "Customer", + "site": "Site", + "contact": "Contact person", + "none": "— none —", + "orderType": "Order type", + "priority": "Priority", + "plannedStart": "Planned start", + "plannedEnd": "Planned end", + "description": "Description", + "scope": "Scope of work", + "internalNotes": "Internal notes (office only)", + "technicianNotes": "Notes for technicians", + "signatureRequired": "Signature required", + "signatureFromType": "As order type", + "yes": "Yes", + "no": "No", + "billingType": "Billing type", + "externalOrderNumber": "External order number", + "offerNumber": "Offer number", + "status": "Create as", + "timezoneHint": "Times in {tz}" + }, + "detail": { + "back": "Back to work orders", + "version": "Version {version}", + "nextStep": "Next step", + "moreActions": "Other status changes", + "edit": "Edit", + "assign": "Assign", + "reassign": "Change assignment", + "cancel": "Cancel order", + "noActions": "No status change is available for you right now.", + "blockersTitle": "Still missing before completion:", + "tabs": { + "overview": "Overview", + "checklist": "Checklist & required photos", + "material": "Material", + "times": "Times", + "photos": "Photos", + "notes": "Notes", + "reports": "Reports", + "documents": "Documents", + "history": "History" + } + }, + "overview": { + "customer": "Customer", + "site": "Site", + "contact": "Contact person", + "planning": "Planning", + "assignment": "Assignment", + "team": "Team", + "teamLead": "Team lead", + "assignees": "Technicians", + "order": "Order", + "created": "Created", + "updated": "Changed", + "accessNotes": "Access", + "safetyNotes": "Safety" + }, + "checklist": { + "items": "Checklist", + "photos": "Required photos", + "empty": "No checklist items.", + "emptyPhotos": "No required photos.", + "required": "Required", + "optional": "Optional", + "requiresPhoto": "with photo", + "done": "Done", + "open": "Open", + "photoCount": "{count, plural, =0 {No photo} one {# photo} other {# photos}}", + "addItem": "Add item", + "addPhoto": "Add required photo", + "label": "Label", + "applyTemplate": "Apply template", + "template": "Template", + "applyButton": "Apply", + "remove": "Remove", + "complete": "All completion requirements met." + }, + "material": { + "title": "Material – planned and actual", + "empty": "No material planned or recorded.", + "name": "Name", + "articleNumber": "Article number", + "planned": "Planned", + "actual": "Actual", + "deviation": "Deviation", + "unit": "Unit", + "notes": "Remark", + "reason": "Reason", + "additional": "Additional", + "notRecorded": "Not recorded yet", + "add": "Plan material", + "remove": "Remove", + "usage": { + "fully_used": "Fully used", + "partially_used": "Partially used", + "not_used": "Not used", + "additional": "Additionally used" + } + }, + "times": { + "empty": "No times recorded yet.", + "person": "Person", + "start": "Start", + "end": "End", + "duration": "Working time", + "minutes": "{minutes} min", + "corrected": "Corrected: {reason}", + "sessionStatus": { + "en_route": "En route", + "running": "Running", + "paused": "Paused", + "ended": "Ended" + }, + "entryType": { + "travel": "Travel", + "work": "Work", + "break": "Break", + "material_procurement": "Material procurement", + "return_travel": "Return travel", + "interruption": "Interruption" + } + }, + "photos": { + "empty": "No photos yet.", + "phase": { "before": "Before", "during": "During", "after": "After" }, + "open": "Open", + "inReport": "In report" + }, + "notes": { + "empty": "No notes.", + "internal": "Internal notes", + "technician": "Notes for technicians", + "kind": { + "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" + } + }, + "reports": { + "empty": "No reports yet.", + "type": { "daily": "Daily report", "completion": "Completion report" }, + "status": { + "draft": "Draft", + "submitted": "In review", + "team_approved": "Approved by team lead", + "approved": "Approved", + "rejected": "Rejected", + "superseded": "Superseded" + }, + "open": "Open report", + "signature": "Signature: {outcome}", + "signatureOutcome": { + "signed": "signed", + "customer_absent": "customer absent", + "refused": "refused", + "later": "later", + "not_required": "not required" + }, + "billingTitle": "Billing", + "billingHint": "Release requires an approved completion report.", + "release": "Release for billing", + "reject": "Send back for correction", + "markBilled": "Mark as billed", + "revoke": "Revoke release" + }, + "documents": { + "empty": "No documents on this order.", + "upload": "Upload document", + "file": "File (PDF, JPG, PNG, WebP)", + "title": "Title (optional)", + "category": "Category", + "visibility": "Visibility", + "submit": "Upload", + "archive": "Remove", + "download": "Download", + "stubHint": "Until the document store is connected, files are stored directly in object storage.", + "categories": { + "order_confirmation": "Order confirmation", + "technical_drawing": "Technical drawing", + "floor_plan": "Floor plan", + "wiring_diagram": "Wiring diagram", + "assembly_instructions": "Assembly instructions", + "safety_document": "Safety document", + "product_document": "Product document", + "customer_note": "Customer note", + "work_record": "Work record", + "daily_report": "Daily report", + "completion_report": "Completion report", + "customer_approval": "Customer approval", + "photo": "Photo", + "voice_note": "Voice note", + "signature": "Signature", + "other": "Other" + }, + "visibilities": { + "backoffice_only": "Office only", + "team_lead": "Team lead", + "team": "Team", + "customer_report": "Customer report" + } + }, + "history": { + "statusChanges": "Status changes", + "audit": "Log", + "empty": "No entries yet.", + "created": "Created as {status}", + "change": "{from} → {to}", + "by": "by {name}", + "system": "System", + "action": { "create": "Created", "update": "Changed", "delete": "Deleted" } + }, + "assign": { + "title": "Assign work order", + "sub": "Choose a team, optionally individual technicians and a team lead", + "team": "Team", + "chooseTeam": "— choose team —", + "members": "Individual technicians (optional)", + "teamLead": "Responsible team lead", + "teamLeadDefault": "Team lead of the team", + "noTeams": "No active teams. Teams are created under “Teams”.", + "submit": "Assign", + "pending": "Assigning…" + }, + "transition": { + "to": { + "draft": "As draft", + "review_required": "Mark for review", + "planned": "Schedule", + "assigned": "Assign", + "accepted": "Accept", + "en_route": "Start travel", + "in_progress": "Start work", + "paused": "Pause", + "waiting_material": "Waiting for material", + "daily_report_created": "Daily report created", + "technically_completed": "Complete technically", + "signature_pending": "Signature pending", + "in_review": "Submit for review", + "released_for_billing": "Release for billing", + "billed": "Mark as billed", + "cancelled": "Cancel order" + }, + "correction": "Send back for correction", + "revoke": "Revoke release", + "reasonTitle": "Reason required", + "reason": "Reason", + "reasonPlaceholder": "Briefly describe why", + "confirm": "Confirm", + "pending": "Saving…", + "done": "Status changed." + }, + "edit": { + "title": "Edit work order", + "submit": "Save", + "pending": "Saving…", + "saved": "Saved." + }, + "conflicts": { + "crumb": "Work orders", + "title": "Sync conflicts", + "sub": "Changes recorded offline that could not be applied automatically", + "empty": "No open conflicts.", + "received": "Received", + "user": "Device / person", + "operation": "Operation", + "entity": "Work order", + "baseVersion": "Device state: version {base} · current: version {current}", + "payload": "Content", + "apply": "Apply", + "discard": "Discard", + "applied": "Applied.", + "discarded": "Discarded.", + "applyHint": "Apply re-runs the operation against the current state – on behalf of the person who recorded it.", + "stubHint": "Until sync is connected, only status changes can be applied." + }, + "errors": { + "not_found": "Not found or no permission.", + "forbidden": "You lack the permission for this.", + "invalid": "Please check your input.", + "conflict": "The order was changed in the meantime. Reload and try again.", + "blocked": "Not possible yet – information is missing.", + "internal": "Operation not possible.", + "validation_failed": "Please check required fields.", + "work_order_not_found": "Work order not found.", + "customer_not_found": "Customer not found.", + "site_not_found": "Site not found.", + "contact_not_found": "Contact person not found.", + "site_customer_mismatch": "The site belongs to another customer.", + "contact_customer_mismatch": "The contact person belongs to another customer.", + "order_type_not_found": "Order type not found.", + "planned_end_before_start": "The end is before the start.", + "nothing_to_update": "No changes.", + "not_editable": "The order can no longer be changed in this status.", + "delete_only_draft": "Only drafts can be deleted.", + "transition_not_allowed": "This status change is not intended.", + "transition_forbidden": "You may not perform this status change.", + "transition_blocked": "Not possible yet – information is missing.", + "reason_required": "Please give a reason.", + "version_conflict": "The order was changed in the meantime. Reload and try again.", + "team_not_found": "Team not found or not active.", + "user_not_found": "At least one person is not active.", + "material_plan_not_found": "Material item not found.", + "material_in_use": "Material has already been recorded for this item.", + "checklist_item_not_found": "Checklist item not found.", + "checklist_item_checked": "Completed items are kept as evidence.", + "photo_requirement_not_found": "Required photo not found.", + "photo_requirement_has_photos": "Photos already exist for this requirement.", + "template_not_found": "Template not found.", + "sync_conflict_not_found": "Conflict not found or already resolved.", + "sync_conflict_already_resolved": "Conflict already resolved.", + "sync_payload_invalid": "The operation is incomplete.", + "sync_user_inactive": "The person is no longer active.", + "reapply_unsupported": "This operation cannot be applied yet.", + "document_not_found": "Document not found.", + "file_type_not_allowed": "File type not allowed.", + "file_too_large": "File is empty or too large.", + "file_missing": "Please choose a file.", + "category_invalid": "Category not allowed.", + "visibility_forbidden": "You may not assign this visibility.", + "initial_status_not_allowed": "This initial status is not allowed." + }, + "blockers": { + "checklist_item": "Checklist: {label}", + "photo_requirement": "Required photo: {label}", + "running_session": "A work session is still running", + "field": { + "signature": "Signature missing", + "approved_completion_report": "Completion report not approved yet" + } + }, + "saved": "Saved." +} diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 0b3318e..68bcef3 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -1,40 +1,170 @@ -import { LayoutDashboard } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; -import { requireSession } from "@/server/auth"; +import { + AlarmClock, + CalendarDays, + CheckCheck, + ClipboardCheck, + ClipboardList, + Hourglass, + PenLine, + Receipt, + RefreshCw, + Siren, + Wrench, + type LucideIcon, +} from "lucide-react"; import { PageHead } from "@/components/mockup-ui"; +import { buttonCls } from "@/components/work-orders/action-form"; +import { pageContext } from "@/components/work-orders/page-context"; +import { Field, inputCls } from "@/components/work-orders/ui"; +import { isoDay, parseListParams, toQuery, type Preset } from "@/lib/work-orders/filters"; +import { WORK_ORDER_PRIORITIES } from "@/lib/work-orders/schemas"; +import { getDashboardTiles } from "@/server/services/work-orders/dashboard"; +import { customerDisplayName, customerFilterOptions, teamOptions, userOptions } from "@/server/services/work-orders/options"; +import { listOrderTypes } from "@/server/services/work-orders/settings"; -/** - * Neutrales Dashboard (Platzhalter). Die Kacheln (offene Aufträge, heutige Einsätze, - * freizugebende Berichte, Notdienst) liefern die Fachmodule. - */ -export default async function DashboardPage({ - searchParams, -}: { - searchParams: Promise<{ module?: string }>; -}) { - const session = await requireSession(); +type SP = Record; + +const TILES: { key: Preset | "sync_conflicts"; icon: LucideIcon; tone: string }[] = [ + { key: "open", icon: ClipboardList, tone: "var(--ui-primary)" }, + { key: "today", icon: CalendarDays, tone: "var(--info)" }, + { key: "running", icon: Wrench, tone: "var(--ui-accent)" }, + { key: "not_accepted", icon: Hourglass, tone: "var(--warn)" }, + { key: "overdue", icon: AlarmClock, tone: "var(--risk)" }, + { key: "reports_in_review", icon: ClipboardCheck, tone: "var(--info)" }, + { key: "completed", icon: CheckCheck, tone: "var(--ok)" }, + { key: "billing", icon: Receipt, tone: "var(--ok)" }, + { key: "emergency_new", icon: Siren, tone: "var(--risk)" }, + { key: "missing_signatures", icon: PenLine, tone: "var(--warn)" }, + { key: "sync_conflicts", icon: RefreshCw, tone: "var(--risk)" }, +]; + +/** Backoffice dashboard (spec §21). Field roles are sent to the mobile start page. */ +export default async function DashboardPage({ searchParams }: { searchParams: Promise }) { + const { session, ctx, can } = await pageContext(); + if (!can("work_order:read_all")) redirect("/m"); const sp = await searchParams; const t = await getTranslations("dashboard"); + const tw = await getTranslations("workOrders"); + + const moduleRow = await ctx.db.tenantModule.findFirst({ where: { moduleKey: "work_orders" }, select: { enabled: true } }); + const moduleEnabled = !moduleRow || moduleRow.enabled; + + const f = parseListParams(sp); + const filter = { from: f.from, to: f.to, customerId: f.customerId, teamId: f.teamId, userId: f.userId, orderTypeId: f.orderTypeId, priority: f.priority }; + const [tiles, customers, teams, users, orderTypes] = moduleEnabled + ? await Promise.all([getDashboardTiles(ctx, filter), customerFilterOptions(ctx), teamOptions(ctx), userOptions(ctx), listOrderTypes(ctx)]) + : [null, [], [], [], []]; return ( -
- +
+ {sp.module === "disabled" && (

{t("moduleDisabled")}

)} -
- -
-

{t("placeholderTitle")}

-

{t("placeholder")}

-
-
+ + {!tiles ? ( +

{t("workOrdersDisabled")}

+ ) : ( + <> +
+ {t("filterTitle")} +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + {tw("filter.reset")} + +
+
+
+ +
    + {TILES.filter((tile) => tile.key !== "sync_conflicts" || can("work_order:write")).map(({ key, icon: Icon, tone }) => { + const count = key === "sync_conflicts" ? tiles.syncConflicts : key === "reports_in_review" ? tiles.reportsToReview : tiles[key]; + const href = key === "sync_conflicts" ? "/work-orders/conflicts" : `/work-orders${toQuery({ ...filter, preset: key })}`; + return ( +
  • + 0 ? tone : "var(--line)" }} + > + +
    +

    {t(`tiles.${key}`)}

    +

    {count}

    +

    {t(`hints.${key}`)}

    +
    + +
  • + ); + })} +
+ + )}
); } diff --git a/src/app/(app)/search/page.tsx b/src/app/(app)/search/page.tsx new file mode 100644 index 0000000..46cc945 --- /dev/null +++ b/src/app/(app)/search/page.tsx @@ -0,0 +1,166 @@ +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { Search } from "lucide-react"; +import { PageHead } from "@/components/mockup-ui"; +import { buttonCls } from "@/components/work-orders/action-form"; +import { pageContext } from "@/components/work-orders/page-context"; +import { Empty, Field, inputCls, Section, StatusBadge } from "@/components/work-orders/ui"; +import { isoDay, parseListParams } from "@/lib/work-orders/filters"; +import { WORK_ORDER_STATUSES } from "@/lib/work-orders/status"; +import { formatDate, formatDateTime } from "@/lib/work-orders/time"; +import { customerDisplayName, teamOptions } from "@/server/services/work-orders/options"; +import { searchAll, type SearchResults } from "@/server/services/work-orders/search"; + +type SP = Record; +const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v); + +/** Tenant search (spec §25) — ILIKE across all areas, restricted by the caller's scopes. */ +export default async function SearchPage({ searchParams }: { searchParams: Promise }) { + const { ctx, locale, tz } = await pageContext(); + const t = await getTranslations("search"); + const tw = await getTranslations("workOrders"); + const td = await getTranslations("workOrders.documents"); + const sp = await searchParams; + const q = (one(sp.q) ?? "").trim().slice(0, 100); + const f = parseListParams(sp); + + const [results, teams] = await Promise.all([ + q.length >= 2 ? searchAll(ctx, { q, from: f.from, to: f.to, statuses: f.statuses, teamId: f.teamId }) : Promise.resolve(null), + teamOptions(ctx), + ]); + const total = results ? Object.values(results).reduce((n, list) => n + list.length, 0) : 0; + const hasFilter = Boolean(f.from || f.to || f.statuses || f.teamId); + + return ( +
+ + +
+
+ + +
+
+ {t("filters.note")} +
+ + + + + + + + + + + + +
+ {hasFilter && ( + + {t("filters.reset")} + + )} +
+
+ +
+ {!results ? ( +

{t("hint")}

+ ) : total === 0 ? ( + {t("noResults", { q })} + ) : ( + <> + + {(w: SearchResults["workOrders"][number]) => ( + + + {w.number} {w.title} + + {[customerDisplayName(w.customer), w.site?.name, w.site?.city, w.team?.name, formatDate(w.plannedStart, locale, tz)].filter(Boolean).join(" · ")} + + + + + )} + + + {(c: SearchResults["customers"][number]) => ( + + {customerDisplayName(c)} + {[c.customerNumber, [c.street, c.houseNumber].filter(Boolean).join(" "), [c.postalCode, c.city].filter(Boolean).join(" ")].filter(Boolean).join(" · ")} + + )} + + + {(s: SearchResults["sites"][number]) => ( + + {s.name} + {[customerDisplayName(s.customer), [s.street, s.houseNumber].filter(Boolean).join(" "), [s.postalCode, s.city].filter(Boolean).join(" ")].filter(Boolean).join(" · ")} + + )} + + + {(c: SearchResults["contacts"][number]) => ( + + {c.name} + {[c.role, customerDisplayName(c.customer), c.phone, c.email].filter(Boolean).join(" · ")} + + )} + + + {(d: SearchResults["documents"][number]) => ( + + {d.title || d.fileName} + {[td(`categories.${d.category}`), formatDate(d.createdAt, locale, tz)].join(" · ")} + + )} + + + {(n: SearchResults["notes"][number]) => ( + + {n.text} + {[n.workOrder.number, n.workOrder.title, formatDateTime(n.createdAt, locale, tz)].join(" · ")} + + )} + +

{t("maxHint")}

+ + )} +
+
+ ); +} + +function ResultSection({ title, list, children }: { title: string; list: T[]; children: (item: T) => React.ReactNode }) { + if (list.length === 0) return null; + return ( +
+
    + {list.map((item) => ( +
  • {children(item)}
  • + ))} +
+
+ ); +} diff --git a/src/app/(app)/settings/checklists/page.tsx b/src/app/(app)/settings/checklists/page.tsx new file mode 100644 index 0000000..9c50ca3 --- /dev/null +++ b/src/app/(app)/settings/checklists/page.tsx @@ -0,0 +1,122 @@ +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { z } from "zod"; +import { Plus } from "lucide-react"; +import { PageHead, Pill } from "@/components/mockup-ui"; +import { Modal } from "@/components/modal"; +import { ActionForm, buttonCls } from "@/components/work-orders/action-form"; +import { SettingsTemplatesNav, templatesPageContext } from "@/components/work-orders/settings-nav"; +import { Check, Empty, Field, inputCls, Section } from "@/components/work-orders/ui"; +import { DEFAULT_CHECKLIST_ITEMS, DEFAULT_REQUIRED_PHOTOS } from "@/lib/work-orders/defaults"; +import { templateItemSchema, templatePhotoSchema, type TemplateItem, type TemplatePhoto } from "@/lib/work-orders/schemas"; +import { saveChecklistTemplateAction } from "@/server/actions/work_orders/settings"; +import { listChecklistTemplates, listOrderTypes } from "@/server/services/work-orders/settings"; + +type SP = Record; +const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v); + +const itemsToText = (items: readonly TemplateItem[], suffix: string) => + items.map((i) => `${i.required ? "* " : ""}${i.label}${i.requiresPhoto ? ` ${suffix}` : ""}`).join("\n"); +const photosToText = (photos: readonly TemplatePhoto[]) => photos.map((p) => p.label).join("\n"); + +/** Checklist templates per order type incl. required photos (spec §12.4 / §14.2). */ +export default async function ChecklistTemplatesPage({ searchParams }: { searchParams: Promise }) { + const { ctx } = await templatesPageContext(); + const t = await getTranslations("settingsTemplates"); + const tc = await getTranslations("common"); + const sp = await searchParams; + const [templates, orderTypes] = await Promise.all([listChecklistTemplates(ctx), listOrderTypes(ctx)]); + + const parsed = templates.map((tpl) => ({ + ...tpl, + itemList: z.array(templateItemSchema).safeParse(tpl.items).data ?? [], + photoList: z.array(templatePhotoSchema).safeParse(tpl.requiredPhotos).data ?? [], + })); + const editId = one(sp.edit); + const editing = editId ? parsed.find((x) => x.id === editId) : undefined; + const isNew = one(sp.new) === "1"; + const suggest = one(sp.suggest) === "1"; + const base = "/settings/checklists"; + + return ( +
+ + + {t("checklists.new")} + + } + /> + + + {parsed.length === 0 ? ( + {t("checklists.empty")} + ) : ( +
    + {parsed.map((tpl) => ( +
  • +
    {t("checklists.inactive")} : undefined}> +

    + {tpl.orderType?.name ?? t("checklists.noOrderType")} · {t("checklists.itemCount", { count: tpl.itemList.length })} · {t("checklists.photoCount", { count: tpl.photoList.length })} +

    + + {t("checklists.edit")} + +
    +
  • + ))} +
+ )} + + {(isNew || editing) && ( + + + {editing && } + + + + + + + +