L6 Benachrichtigungen & Audit: Empfängerregeln, In-App-Benachrichtigungen und Craftvia-Mails

- handleEvent (Signatur unverändert) löst alle 17 Domain-Events auf: Team/Assignees,
  Backoffice (read_all + report:approve), Teamleiter-Freigaben, Ersteller, Abrechnung,
  Notdienst, Import, Sync; Akteur ausgenommen, alle IDs über ctx.db neu aufgelöst.
- In-App-Notification über ctx.db (ungelesene gleiche Meldung wird aufgefrischt),
  E-Mail über enqueueMail mit dedupeKey event:entity:user (+ optional occurrenceId),
  Opt-out je Typ, Notdienst als Pflichtmail, feste Empfänger ohne Doppelmail.
- Mail-Templates craftvia_* (de/en) inkl. Notdienst-Format Spec §19.4 und Craftvia-Fußzeile.
- Migration tenant_mail_settings: mailFromName, mailReplyTo, emergencyRecipients,
  billingRecipients an tenant_settings (keine neue Tabelle).
- Texte aus messages/{de,en}/notifications.json.
- Test scripts/test-benachrichtigungen-events.ts (63 Prüfungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:11:53 +02:00
co-authored by Claude Opus 5
parent bf4456718e
commit 7b37c41a83
9 changed files with 1452 additions and 4 deletions
+183
View File
@@ -0,0 +1,183 @@
{
"bell": {
"label": "Benachrichtigungen",
"unreadCount": "{count, plural, =0 {Keine ungelesenen} one {# ungelesen} other {# ungelesen}}",
"empty": "Keine Benachrichtigungen.",
"markAllRead": "Alle gelesen",
"showAll": "Alle anzeigen"
},
"list": {
"crumb": "Übersicht",
"title": "Benachrichtigungen",
"sub": "Zuweisungen, Berichte, Notdienst und Fehler – neueste zuerst.",
"filterStatus": "Status",
"filterType": "Art",
"statusAll": "Alle",
"statusUnread": "Ungelesen",
"statusRead": "Gelesen",
"typeAll": "Alle Arten",
"apply": "Filtern",
"reset": "Zurücksetzen",
"empty": "Keine Benachrichtigungen für diese Auswahl.",
"unread": "Neu",
"read": "Gelesen",
"open": "Öffnen",
"markRead": "Als gelesen markieren",
"markAllRead": "Alle als gelesen markieren",
"previous": "Zurück",
"next": "Weiter",
"page": "Seite {page} von {pages}"
},
"types": {
"work_order_assigned": "Teamzuweisung",
"work_order_changed": "Auftrag geändert",
"work_order_cancelled": "Auftrag storniert",
"work_order_started": "Auftrag gestartet",
"work_order_daily_report_created": "Tagesbericht erstellt",
"work_order_technically_completed": "Technisch abgeschlossen",
"work_order_signature_missing": "Unterschrift fehlt",
"work_order_missing_required": "Pflichtangaben fehlen",
"work_order_released_for_billing": "Zur Abrechnung freigegeben",
"report_submitted": "Bericht zur Prüfung",
"report_approved": "Bericht freigegeben",
"report_rejected": "Bericht zurückgewiesen",
"emergency_created": "Notdiensteinsatz erstellt",
"emergency_completed": "Notdiensteinsatz abgeschlossen",
"import_ready_for_review": "Import zur Prüfung",
"import_failed": "Fehler bei Dokumentverarbeitung",
"sync_failed": "Synchronisationsfehler"
},
"events": {
"work_order_assigned": { "title": "Neuer Auftrag {number}", "message": "{title} – {customer} wurde Ihrem Team zugewiesen." },
"work_order_changed": { "title": "Auftrag {number} geändert", "message": "{title} – {customer}: Angaben wurden aktualisiert." },
"work_order_cancelled": { "title": "Auftrag {number} storniert", "message": "{title} – {customer} wurde storniert." },
"work_order_started": { "title": "Auftrag {number} gestartet", "message": "{actor} hat den Einsatz bei {customer} begonnen." },
"work_order_daily_report_created": { "title": "Tagesbericht zu {number}", "message": "{actor} hat einen Tagesbericht für {customer} erstellt." },
"work_order_technically_completed": { "title": "Auftrag {number} technisch abgeschlossen", "message": "{title} – {customer}: Einsatz vor Ort beendet." },
"work_order_signature_missing": { "title": "Unterschrift fehlt: {number}", "message": "{title} – {customer}: Kundenunterschrift liegt nicht vor." },
"work_order_missing_required": { "title": "Pflichtangaben fehlen: {number}", "message": "{title} – {customer}: Dokumentation unvollständig." },
"work_order_released_for_billing": { "title": "Auftrag {number} zur Abrechnung", "message": "{title} – {customer} ist bereit zur Abrechnung." },
"report_submitted": { "title": "Bericht zur Prüfung: {number}", "message": "{actor} hat einen Bericht zu {title} eingereicht." },
"report_approved": { "title": "Bericht freigegeben: {number}", "message": "Der Bericht zu {title} wurde freigegeben." },
"report_rejected": { "title": "Bericht zurückgewiesen: {number}", "message": "Der Bericht zu {title} braucht eine Korrektur." },
"emergency_created": { "title": "Notdiensteinsatz {number} erstellt", "message": "{actor} hat einen Notdiensteinsatz bei {customer} angelegt." },
"emergency_completed": { "title": "Notdiensteinsatz {number} abgeschlossen", "message": "{customer}: zur Prüfung und Abrechnung." },
"import_ready_for_review": { "title": "Import zur Prüfung bereit", "message": "Die Daten aus {fileName} liegen zur Prüfung vor." },
"import_failed": { "title": "Fehler bei Dokumentverarbeitung", "message": "{fileName} konnte nicht verarbeitet werden." },
"sync_failed": { "title": "Synchronisation fehlgeschlagen", "message": "Eine Änderung konnte nicht übernommen werden ({reason})." }
},
"fallback": {
"unknown": "unbekannt",
"system": "System",
"document": "Dokument"
},
"preferences": {
"title": "Benachrichtigungen",
"sub": "In der App erhalten Sie alle Benachrichtigungen. Hier legen Sie fest, welche zusätzlich per E-Mail kommen.",
"email": "E-Mail",
"mandatory": "Pflicht – nicht abbestellbar",
"save": "Speichern",
"saved": "Einstellungen gespeichert."
},
"mailSettings": {
"crumb": "Einstellungen",
"title": "E-Mail-Versand",
"sub": "Absender, Antwortadresse und feste Empfänger für Notdienst und Abrechnung.",
"back": "Zurück zu Einstellungen",
"sender": "Absender",
"fromName": "Absendername",
"fromNameHint": "Erscheint als Name im Posteingang. Leer = Firmenname.",
"fromAddress": "Absenderadresse",
"fromAddressHint": "Die Adresse bleibt {address}, damit Mails zugestellt werden (SPF/DKIM).",
"replyTo": "Antwortadresse",
"replyToHint": "Antworten auf Benachrichtigungen gehen an diese Adresse.",
"recipients": "Feste Empfänger",
"emergencyRecipients": "Empfänger Notdienst",
"emergencyHint": "Eine Adresse je Zeile. Erhalten jeden neuen und abgeschlossenen Notdiensteinsatz.",
"billingRecipients": "Empfänger Abrechnung",
"billingHint": "Eine Adresse je Zeile. Erhalten Aufträge, die zur Abrechnung freigegeben sind. Leer = Backoffice.",
"save": "Speichern",
"saved": "Einstellungen gespeichert.",
"invalid": "Bitte Eingaben prüfen: {detail}"
},
"audit": {
"crumb": "Einstellungen",
"title": "Audit-Protokoll",
"sub": "Wer hat wann was geändert – nur lesbar.",
"back": "Zurück zu Einstellungen",
"from": "Von",
"to": "Bis",
"actor": "Benutzer",
"actorAll": "Alle Benutzer",
"action": "Aktion",
"actionAll": "Alle Aktionen",
"entity": "Objektart",
"entityAll": "Alle Objektarten",
"entityId": "Objekt-ID",
"apply": "Filtern",
"reset": "Zurücksetzen",
"time": "Zeitpunkt",
"object": "Objekt",
"result": "Ergebnis",
"resultOk": "Erfolgreich",
"resultDenied": "Abgelehnt",
"details": "Details",
"empty": "Keine Einträge für diese Auswahl.",
"total": "{count, plural, one {# Eintrag} other {# Einträge}}",
"previous": "Zurück",
"next": "Weiter",
"page": "Seite {page} von {pages}",
"system": "System",
"platform": "Plattform",
"detailTitle": "Eintrag im Audit-Protokoll",
"close": "Schließen",
"field": "Feld",
"before": "Vorher",
"after": "Nachher",
"changed": "geändert",
"noValues": "Keine Werte protokolliert.",
"ip": "IP-Adresse",
"userAgent": "User-Agent",
"notCaptured": "nicht erfasst",
"actions": {
"create": "Erstellt",
"update": "Geändert",
"delete": "Gelöscht",
"login": "Anmeldung",
"logout": "Abmeldung",
"denied": "Abgelehnt",
"export": "Export",
"import": "Import",
"provision": "Eingerichtet",
"approve": "Freigegeben",
"reject": "Zurückgewiesen"
},
"entities": {
"user": "Benutzer",
"role": "Rolle",
"tenant": "Mandant",
"tenant_settings": "Einstellungen",
"tenant_mail_settings": "E-Mail-Versand",
"tenant_module": "Modul",
"module": "Modul",
"session": "Sitzung",
"account_inactive": "Inaktives Konto",
"work_order": "Auftrag",
"customer": "Kunde",
"site": "Objekt",
"team": "Team",
"report": "Bericht",
"signature": "Unterschrift",
"document": "Dokument",
"import_job": "Auftragsimport",
"material_usage": "Material",
"time_entry": "Arbeitszeit",
"work_session": "Einsatzzeit",
"photo": "Foto",
"voice_note": "Sprachnotiz",
"notification": "Benachrichtigung",
"notification_settings": "Benachrichtigungseinstellungen",
"sync_operation": "Synchronisation"
}
}
}
+183
View File
@@ -0,0 +1,183 @@
{
"bell": {
"label": "Notifications",
"unreadCount": "{count, plural, =0 {No unread} one {# unread} other {# unread}}",
"empty": "No notifications.",
"markAllRead": "Mark all read",
"showAll": "Show all"
},
"list": {
"crumb": "Overview",
"title": "Notifications",
"sub": "Assignments, reports, emergency call-outs and errors – newest first.",
"filterStatus": "Status",
"filterType": "Type",
"statusAll": "All",
"statusUnread": "Unread",
"statusRead": "Read",
"typeAll": "All types",
"apply": "Filter",
"reset": "Reset",
"empty": "No notifications for this selection.",
"unread": "New",
"read": "Read",
"open": "Open",
"markRead": "Mark as read",
"markAllRead": "Mark all as read",
"previous": "Previous",
"next": "Next",
"page": "Page {page} of {pages}"
},
"types": {
"work_order_assigned": "Team assignment",
"work_order_changed": "Work order changed",
"work_order_cancelled": "Work order cancelled",
"work_order_started": "Work order started",
"work_order_daily_report_created": "Daily report created",
"work_order_technically_completed": "Technically completed",
"work_order_signature_missing": "Signature missing",
"work_order_missing_required": "Required information missing",
"work_order_released_for_billing": "Released for billing",
"report_submitted": "Report for review",
"report_approved": "Report approved",
"report_rejected": "Report rejected",
"emergency_created": "Emergency call-out created",
"emergency_completed": "Emergency call-out completed",
"import_ready_for_review": "Import ready for review",
"import_failed": "Document processing failed",
"sync_failed": "Sync failed"
},
"events": {
"work_order_assigned": { "title": "New work order {number}", "message": "{title} – {customer} was assigned to your team." },
"work_order_changed": { "title": "Work order {number} changed", "message": "{title} – {customer}: details were updated." },
"work_order_cancelled": { "title": "Work order {number} cancelled", "message": "{title} – {customer} was cancelled." },
"work_order_started": { "title": "Work order {number} started", "message": "{actor} started work at {customer}." },
"work_order_daily_report_created": { "title": "Daily report for {number}", "message": "{actor} created a daily report for {customer}." },
"work_order_technically_completed": { "title": "Work order {number} technically completed", "message": "{title} – {customer}: on-site work finished." },
"work_order_signature_missing": { "title": "Signature missing: {number}", "message": "{title} – {customer}: no customer signature." },
"work_order_missing_required": { "title": "Required information missing: {number}", "message": "{title} – {customer}: documentation incomplete." },
"work_order_released_for_billing": { "title": "Work order {number} ready for billing", "message": "{title} – {customer} is ready for billing." },
"report_submitted": { "title": "Report for review: {number}", "message": "{actor} submitted a report for {title}." },
"report_approved": { "title": "Report approved: {number}", "message": "The report for {title} was approved." },
"report_rejected": { "title": "Report rejected: {number}", "message": "The report for {title} needs a correction." },
"emergency_created": { "title": "Emergency call-out {number} created", "message": "{actor} created an emergency call-out at {customer}." },
"emergency_completed": { "title": "Emergency call-out {number} completed", "message": "{customer}: ready for review and billing." },
"import_ready_for_review": { "title": "Import ready for review", "message": "The data from {fileName} is ready for review." },
"import_failed": { "title": "Document processing failed", "message": "{fileName} could not be processed." },
"sync_failed": { "title": "Sync failed", "message": "A change could not be applied ({reason})." }
},
"fallback": {
"unknown": "unknown",
"system": "System",
"document": "document"
},
"preferences": {
"title": "Notifications",
"sub": "You receive all notifications in the app. Choose which ones are also sent by e-mail.",
"email": "E-mail",
"mandatory": "Mandatory – cannot be turned off",
"save": "Save",
"saved": "Settings saved."
},
"mailSettings": {
"crumb": "Settings",
"title": "E-mail delivery",
"sub": "Sender, reply-to address and fixed recipients for emergency call-outs and billing.",
"back": "Back to settings",
"sender": "Sender",
"fromName": "Sender name",
"fromNameHint": "Shown as the name in the inbox. Empty = company name.",
"fromAddress": "Sender address",
"fromAddressHint": "The address stays {address} so that mail is delivered (SPF/DKIM).",
"replyTo": "Reply-to address",
"replyToHint": "Replies to notifications go to this address.",
"recipients": "Fixed recipients",
"emergencyRecipients": "Emergency recipients",
"emergencyHint": "One address per line. Receive every new and completed emergency call-out.",
"billingRecipients": "Billing recipients",
"billingHint": "One address per line. Receive work orders released for billing. Empty = back office.",
"save": "Save",
"saved": "Settings saved.",
"invalid": "Please check your input: {detail}"
},
"audit": {
"crumb": "Settings",
"title": "Audit log",
"sub": "Who changed what and when – read only.",
"back": "Back to settings",
"from": "From",
"to": "To",
"actor": "User",
"actorAll": "All users",
"action": "Action",
"actionAll": "All actions",
"entity": "Object type",
"entityAll": "All object types",
"entityId": "Object ID",
"apply": "Filter",
"reset": "Reset",
"time": "Time",
"object": "Object",
"result": "Result",
"resultOk": "Succeeded",
"resultDenied": "Denied",
"details": "Details",
"empty": "No entries for this selection.",
"total": "{count, plural, one {# entry} other {# entries}}",
"previous": "Previous",
"next": "Next",
"page": "Page {page} of {pages}",
"system": "System",
"platform": "Platform",
"detailTitle": "Audit log entry",
"close": "Close",
"field": "Field",
"before": "Before",
"after": "After",
"changed": "changed",
"noValues": "No values recorded.",
"ip": "IP address",
"userAgent": "User agent",
"notCaptured": "not recorded",
"actions": {
"create": "Created",
"update": "Updated",
"delete": "Deleted",
"login": "Sign-in",
"logout": "Sign-out",
"denied": "Denied",
"export": "Export",
"import": "Import",
"provision": "Provisioned",
"approve": "Approved",
"reject": "Rejected"
},
"entities": {
"user": "User",
"role": "Role",
"tenant": "Tenant",
"tenant_settings": "Settings",
"tenant_mail_settings": "E-mail delivery",
"tenant_module": "Module",
"module": "Module",
"session": "Session",
"account_inactive": "Inactive account",
"work_order": "Work order",
"customer": "Customer",
"site": "Site",
"team": "Team",
"report": "Report",
"signature": "Signature",
"document": "Document",
"import_job": "Work order import",
"material_usage": "Material",
"time_entry": "Working time",
"work_session": "Work session",
"photo": "Photo",
"voice_note": "Voice note",
"notification": "Notification",
"notification_settings": "Notification settings",
"sync_operation": "Sync"
}
}
}