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.
87 lines
5.0 KiB
HTML
87 lines
5.0 KiB
HTML
{{define "content"}}
|
|
|
|
{{if .Overdue}}
|
|
<h1 style="margin:0 0 12px 0;font-size:20px;line-height:1.3;color:#b91c1c;">
|
|
Überfällig ({{.OverdueCount}})
|
|
</h1>
|
|
<p style="margin:0 0 12px 0;color:#7f1d1d;font-weight:600;">
|
|
Systemausfall: diese Fristen wurden nicht rechtzeitig erledigt. Der Eskalations­kontakt wurde informiert.
|
|
</p>
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px 0;border:1px solid #fecaca;border-radius:6px;border-collapse:separate;border-spacing:0;">
|
|
<tr style="background:#fef2f2;">
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#7f1d1d;font-weight:600;border-bottom:1px solid #fecaca;">Fällig</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#7f1d1d;font-weight:600;border-bottom:1px solid #fecaca;">Titel</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#7f1d1d;font-weight:600;border-bottom:1px solid #fecaca;">Akte</th>
|
|
</tr>
|
|
{{range .Overdue}}
|
|
<tr>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #fee2e2;white-space:nowrap;color:#b91c1c;font-weight:600;">{{.DueDate}}</td>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #fee2e2;">
|
|
<a href="{{.URL}}" style="color:#1c1917;text-decoration:none;font-weight:500;">{{.Title}}</a>
|
|
{{if .IsOtherOwner}}<div style="font-size:11px;color:#78716c;">Eigentümer: {{.OwnerName}}</div>{{end}}
|
|
</td>
|
|
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #fee2e2;">
|
|
{{.ProjectReference}}{{if .ProjectTitle}} — {{.ProjectTitle}}{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{end}}
|
|
|
|
{{if .DueToday}}
|
|
<h2 style="margin:0 0 12px 0;font-size:18px;line-height:1.3;color:#b45309;">
|
|
{{if .IsEvening}}DRINGEND — heute noch offen ({{.DueTodayCount}}){{else}}Heute fällig ({{.DueTodayCount}}){{end}}
|
|
</h2>
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px 0;border:1px solid #fde68a;border-radius:6px;border-collapse:separate;border-spacing:0;">
|
|
<tr style="background:#fffbeb;">
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#92400e;font-weight:600;border-bottom:1px solid #fde68a;">Fällig</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#92400e;font-weight:600;border-bottom:1px solid #fde68a;">Titel</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#92400e;font-weight:600;border-bottom:1px solid #fde68a;">Akte</th>
|
|
</tr>
|
|
{{range .DueToday}}
|
|
<tr>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #fef3c7;white-space:nowrap;">{{.DueDate}}</td>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #fef3c7;">
|
|
<a href="{{.URL}}" style="color:#1c1917;text-decoration:none;font-weight:500;">{{.Title}}</a>
|
|
{{if .IsOtherOwner}}<div style="font-size:11px;color:#78716c;">Eigentümer: {{.OwnerName}}</div>{{end}}
|
|
</td>
|
|
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #fef3c7;">
|
|
{{.ProjectReference}}{{if .ProjectTitle}} — {{.ProjectTitle}}{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{end}}
|
|
|
|
{{if .DueWarning}}
|
|
<h2 style="margin:0 0 12px 0;font-size:18px;line-height:1.3;color:#1c1917;">
|
|
In einer Woche fällig ({{.DueWarningCount}})
|
|
</h2>
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px 0;border:1px solid #e7e5e4;border-radius:6px;border-collapse:separate;border-spacing:0;">
|
|
<tr style="background:#f5f5f4;">
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">Fällig</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">Titel</th>
|
|
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">Akte</th>
|
|
</tr>
|
|
{{range .DueWarning}}
|
|
<tr>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #f5f5f4;white-space:nowrap;">{{.DueDate}}</td>
|
|
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #f5f5f4;">
|
|
<a href="{{.URL}}" style="color:#1c1917;text-decoration:none;font-weight:500;">{{.Title}}</a>
|
|
{{if .IsOtherOwner}}<div style="font-size:11px;color:#78716c;">Eigentümer: {{.OwnerName}}</div>{{end}}
|
|
</td>
|
|
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #f5f5f4;">
|
|
{{.ProjectReference}}{{if .ProjectTitle}} — {{.ProjectTitle}}{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{end}}
|
|
|
|
<p style="margin:24px 0 0 0;">
|
|
<a href="{{.DeadlinesURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
|
Alle Fristen
|
|
</a>
|
|
</p>
|
|
{{end}}
|