Files
paliad/internal/services/email_template_samples.go
m 0e3411c40b feat(admin): /admin/email-templates editor (t-paliad-072)
DB-backed email-template editor for global_admins, replacing the
"Kommt bald" placeholder. Admins can edit invitation, deadline_digest,
and the shared base wrapper for both DE and EN, preview against sample
data, save with versions, and reset to the embedded default.

Backend:
- Migration 026 adds paliad.email_templates (active row per (key, lang))
  and paliad.email_template_versions (append-only, retained 20 deep).
- EmailTemplateService — GetActive falls through to the embedded per-
  language file when no DB row, Save validates parse + structural
  invariants and writes a version, Reset deletes the active row, Restore
  copies a version back. Mutations require DB; reads work without.
- MailService now consults the service for body and subject and falls
  back to the embedded default if the active row is malformed at parse
  time — a corrupt admin save can never wedge the send path.
- Subjects move from Go (buildDigestSubject + inviteSubject) to
  text/template strings stored in the (key, lang) row. Default subjects
  ship with a {{/* keep this phrasing */}} comment pointing at the
  reminder-redesign doc so the SLO framing rationale survives edits.
- Bilingual templates split into per-language files (invitation.de.html
  + .en.html, deadline_digest.de.html + .en.html, base.de.html + .en.html).
  No more {{if eq .Lang}} branching inside templates.
- Handlers under /api/admin/email-templates/* gated by the existing
  RequireAdminFunc(users) admin middleware, same shape as /admin/team.

Frontend:
- /admin/email-templates list page — three cards (one per template),
  each linking to DE + EN editors with their last-modified status.
- /admin/email-templates/{key}?lang=de three-pane editor — subject + body
  textarea + variable docs + actions on the left, sandboxed iframe
  preview + version log on the right. 500 ms debounced live preview;
  save validates server-side (422 on parse error, surfaced inline).
- admin.tsx flips the Email-Templates card from PLANNED to verfügbar.
- 50 new i18n keys (DE + EN) for the editor surface.

Tests: GetActive fallback path, ValidateTemplate happy + sad paths,
SaveRequiresStore on no-DB service, RenderTemplate body + subject
goldens, full SYSTEMAUSFALL/SYSTEM FAILURE subject matrix.

Smoke (knowledge-platform-only run, no DB/auth):
- GET /admin/email-templates → 302 to /login
- GET /api/admin/email-templates → 401
- go build/vet/test clean, bun run build clean

Design: docs/design-email-templates-2026-04-29.md.
2026-04-29 22:09:39 +02:00

117 lines
3.8 KiB
Go

// Sample data for the /admin/email-templates preview pane. Each preview
// request renders the proposed subject + body against this fixed payload
// so the admin sees the layout exactly as it will look in production for a
// representative case. Sample data is server-authoritative; the editor
// can't override it (out of scope for v1 — see design doc §9).
package services
// EmailTemplateSampleData returns a fresh sample payload for (key, lang).
// `slot` is honoured for deadline_digest only ("morning" / "evening"); other
// keys ignore it.
func EmailTemplateSampleData(key, lang, slot string) map[string]any {
switch key {
case EmailTemplateKeyInvitation:
return invitationSample(lang)
case EmailTemplateKeyDeadlineDigest:
return deadlineDigestSample(lang, slot)
case EmailTemplateKeyBase:
return baseSample(lang)
default:
return map[string]any{}
}
}
func invitationSample(lang string) map[string]any {
if lang == "en" {
return map[string]any{
"InviterName": "Maria Schmidt",
"InviterEmail": "maria.schmidt@hlc.com",
"ToEmail": "new.colleague@hlc.com",
"Message": "Hi — I think you'd find Paliad useful. Have a look.",
"RegisterURL": "https://paliad.de/login",
}
}
return map[string]any{
"InviterName": "Maria Schmidt",
"InviterEmail": "maria.schmidt@hlc.com",
"ToEmail": "neu.kollege@hlc.de",
"Message": "Hallo Kolleg:in — ich glaube Paliad wäre nützlich für dich. Schau es dir an.",
"RegisterURL": "https://paliad.de/login",
}
}
func deadlineDigestSample(lang, slot string) map[string]any {
isEvening := slot == "evening"
overdue := []map[string]any{
{
"DueDate": "2026-04-27",
"Title": ifLang(lang, "Beschwerde gegen EP-Anmeldung einreichen", "File appeal against EP application"),
"ProjectReference": "HL-2024-0083",
"ProjectTitle": "Acme vs Beta GmbH",
"OwnerName": "Maria Schmidt",
"IsOtherOwner": true,
"URL": "https://paliad.de/deadlines/sample-overdue-1",
},
}
dueToday := []map[string]any{
{
"DueDate": "2026-04-29",
"Title": ifLang(lang, "Klageerwiderung einreichen", "File reply to complaint"),
"ProjectReference": "HL-2025-0011",
"ProjectTitle": "Gamma AG vs Delta Inc",
"OwnerName": "Self",
"IsOtherOwner": false,
"URL": "https://paliad.de/deadlines/sample-due-today-1",
},
{
"DueDate": "2026-04-29",
"Title": ifLang(lang, "Vollmacht prüfen und gegenzeichnen", "Review and counter-sign power of attorney"),
"ProjectReference": "HL-2025-0014",
"ProjectTitle": "",
"OwnerName": "Self",
"IsOtherOwner": false,
"URL": "https://paliad.de/deadlines/sample-due-today-2",
},
}
dueWarning := []map[string]any{
{
"DueDate": "2026-05-06",
"Title": ifLang(lang, "Stellungnahme zur Erteilung vorbereiten", "Prepare response to grant"),
"ProjectReference": "HL-2025-0007",
"ProjectTitle": "Epsilon Ltd",
"OwnerName": "Self",
"IsOtherOwner": false,
"URL": "https://paliad.de/deadlines/sample-warning-1",
},
}
return map[string]any{
"Slot": slot,
"IsEvening": isEvening,
"Overdue": overdue,
"OverdueCount": len(overdue),
"DueToday": dueToday,
"DueTodayCount": len(dueToday),
"DueWarning": dueWarning,
"DueWarningCount": len(dueWarning),
"OpenTotal": len(dueToday) + len(dueWarning),
"DeadlinesURL": "https://paliad.de/deadlines",
}
}
func baseSample(lang string) map[string]any {
subj := "Beispielbetreff"
if lang == "en" {
subj = "Example subject"
}
return map[string]any{
"Subject": subj,
}
}
func ifLang(lang, de, en string) string {
if lang == "en" {
return en
}
return de
}