feat(notifications): renderNotificationEmail + previewEmailTemplate + logistics sample data

- Export `renderNotificationEmail` from email.ts (pure template resolver, no send)
- Refactor `sendStyledNotificationEmail` to delegate to `renderNotificationEmail`
- Hoist sampleData to module-level `NOTIFICATION_SAMPLE_DATA` in notification router
- Add 8 logistics sample entries (FINALIST_*/TRAVEL_CONFIRMED/VISA_STATUS_UPDATE)
- Add `notification.previewEmailTemplate` adminProcedure query (returns subject/html/hasStyledTemplate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-04 16:39:29 +02:00
parent 27bdf8cdef
commit e5788b3e9d
3 changed files with 230 additions and 124 deletions

View File

@@ -336,7 +336,7 @@ function statCard(label: string, value: string | number): string {
// Email Templates
// =============================================================================
interface EmailTemplate {
export interface EmailTemplate {
subject: string
html: string
text: string
@@ -2689,6 +2689,23 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record<string, TemplateGenerator> = {
),
}
/**
* Resolve the email template for a notification type without sending.
* Exported for use by preview endpoints.
*/
export function renderNotificationEmail(
name: string,
type: string,
context: NotificationEmailContext,
subjectOverride?: string,
): EmailTemplate {
const generator = NOTIFICATION_EMAIL_TEMPLATES[type]
const template = generator
? generator({ ...context, linkUrl: ensureAbsoluteUrl(context.linkUrl), name })
: getNotificationEmailTemplate(name, subjectOverride || context.title, context.message, ensureAbsoluteUrl(context.linkUrl))
return subjectOverride ? { ...template, subject: subjectOverride } : template
}
/**
* Send styled notification email using the appropriate template
*/
@@ -2699,32 +2716,7 @@ export async function sendStyledNotificationEmail(
context: NotificationEmailContext,
subjectOverride?: string
): Promise<void> {
// Safety net: always ensure linkUrl is absolute before passing to templates
const safeContext = {
...context,
linkUrl: ensureAbsoluteUrl(context.linkUrl),
}
const templateGenerator = NOTIFICATION_EMAIL_TEMPLATES[type]
let template: EmailTemplate
if (templateGenerator) {
// Use styled template
template = templateGenerator({ ...safeContext, name })
// Apply subject override if provided
if (subjectOverride) {
template.subject = subjectOverride
}
} else {
// Fall back to generic template
template = getNotificationEmailTemplate(
name,
subjectOverride || safeContext.title,
safeContext.message,
safeContext.linkUrl
)
}
const template = renderNotificationEmail(name, type, context, subjectOverride)
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
}