fix(projects): scope email rollup to CRM types + re-render unstored previews

- The customer address often doubles as the admin notification target, so
  matching emails purely by recipient swept in system alerts (backup_failed,
  restore_failed, …). The recipient match is now restricted to CRM document
  types (quote_/contract_/invoice_/storno_); event-scoped mails still match
  by event_id.
- getEmailPreview now falls back to renderQueuedEmail() — re-rendering from
  the current template + the row's stored email_data — for emails sent before
  rendered_html capture, flagged exact:false with an amber 're-rendered' note.
  Only a missing template / no variables falls through to 'nothing stored'.

Known limitation: a customer with multiple projects sees their event_id=null
CRM mails under each (email_queue has no project_id).
This commit is contained in:
Luca
2026-06-06 22:47:43 +02:00
parent c0b6d14d08
commit f02fba6332
6 changed files with 76 additions and 14 deletions
+2 -1
View File
@@ -3439,7 +3439,8 @@
"cancel": "Abbrechen",
"retry": "Wiederholen",
"previewTitle": "E-Mail-Vorschau",
"noPreview": "Keine gespeicherte Vorschau für diese E-Mail — sie wurde gesendet, bevor Vorschauen erfasst wurden."
"noPreview": "Keine gespeicherte Vorschau für diese E-Mail — sie wurde gesendet, bevor Vorschauen erfasst wurden.",
"reRendered": "Neu gerendert aus der aktuellen Vorlage — diese E-Mail wurde gesendet, bevor Vorschauen erfasst wurden, und kann daher leicht von der tatsächlich versendeten abweichen."
},
"toast": {
"created": "Projekt erstellt",
+2 -1
View File
@@ -3439,7 +3439,8 @@
"cancel": "Cancel",
"retry": "Retry",
"previewTitle": "Email preview",
"noPreview": "No stored preview for this email — it was sent before previews were captured."
"noPreview": "No stored preview for this email — it was sent before previews were captured.",
"reRendered": "Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received."
},
"toast": {
"created": "Project created",
@@ -404,7 +404,14 @@ export const ProjectCockpitPage: React.FC = () => {
{previewLoading ? (
<Loading />
) : preview && preview.available && preview.html ? (
<iframe title="email-preview" srcDoc={preview.html} className="w-full h-[60vh] border border-neutral-200 dark:border-neutral-700 rounded bg-white" />
<>
{!preview.exact && (
<div className="mb-3 rounded-md bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 px-3 py-2 text-xs text-amber-800 dark:text-amber-200">
{t('projects.email.reRendered', 'Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received.')}
</div>
)}
<iframe title="email-preview" srcDoc={preview.html} className="w-full h-[60vh] border border-neutral-200 dark:border-neutral-700 rounded bg-white" />
</>
) : (
<div className="text-center py-10 text-neutral-500">
{t('projects.email.noPreview', 'No stored preview for this email — it was sent before previews were captured.')}
@@ -118,6 +118,9 @@ export interface EmailPreview {
type: string;
status: string;
available: boolean;
/** true = exact bytes stored at send time; false = re-rendered from the
* current template (approximation for emails sent before capture). */
exact: boolean;
html: string | null;
}