Files
pn-new-crm/src/lib/email/shell.ts

81 lines
3.2 KiB
TypeScript
Raw Normal View History

feat(branding): wire per-port branding through every transactional email + auth shell (R2-H15) Multi-tenant branding admin (/admin/branding) was saving 5 settings that no code read — every port's emails shipped Port Nimara's logo and color regardless. Now wired end-to-end: New shared infrastructure: - src/lib/email/shell.ts — renderShell() + brandingPrimaryColor() helpers; takes BrandingShell { logoUrl, primaryColor, emailHeaderHtml, emailFooterHtml }, falls back to Port Nimara defaults when null. - src/lib/email/branding-resolver.ts — getBrandingShell(portId) thin wrapper over getPortBrandingConfig() that returns null on error / missing portId so senders never break on misconfig. All 6 transactional templates refactored to use renderShell + the shared accent color; portName now flows through every template (crm-invite, portal activation/reset, both inquiries, both residential templates, notification digest). All 6 senders pass branding via getBrandingShell: - portal-auth.service.ts (activation + reset) - crm-invite.service.ts (resend path; create-invite has no portId yet so falls through to defaults) - email worker (inquiry confirmation + sales notification) - residential-inquiries route (client confirmation + sales alert) - notification-digest.service.ts (digest) BrandedAuthShell takes an optional `branding` prop with logoUrl + appName (parent page server-fetches via getPortBrandingConfig). Defaults to Port Nimara if omitted, so single-tenant deployments are unaffected. 1175/1175 vitest passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:00:45 +02:00
/**
* Shared HTML shell for transactional emails. Centralises the table-
* based layout + the per-port branding override surface so templates
* don't each inline a different copy of the boilerplate.
*
* Per-port branding (R2-H15):
* - logoUrl replaces the default Port Nimara logo image
* - primaryColor used for the page-title accent color
* - emailHeaderHtml / emailFooterHtml admin-authored HTML that
* appears above / below the body content (e.g. legal footer,
* custom marketing strip). When unset, the existing minimal
* "Thank you, {{portName}} CRM" sign-off is rendered by callers.
*
* Senders resolve a `BrandingShell` via `resolveBrandingShell(portId)`
* (or pass `null` for no override) and forward it to the template
* function. Templates call `renderShell({ title, body, branding })`.
*/
const DEFAULT_LOGO_URL =
'https://s3.portnimara.com/images/Port%20Nimara%20New%20Logo-Circular%20Frame_250px.png';
const DEFAULT_BACKGROUND_URL = 'https://s3.portnimara.com/images/Overhead_1_blur.png';
const DEFAULT_PRIMARY_COLOR = '#0F4C81';
export interface BrandingShell {
logoUrl: string | null;
primaryColor: string | null;
emailHeaderHtml: string | null;
emailFooterHtml: string | null;
}
interface ShellOpts {
title: string;
body: string;
branding?: BrandingShell | null;
}
export function renderShell({ title, body, branding }: ShellOpts): string {
const logoUrl = branding?.logoUrl ?? DEFAULT_LOGO_URL;
const headerHtml = branding?.emailHeaderHtml ?? '';
const footerHtml = branding?.emailFooterHtml ?? '';
return `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>${title}</title>
<style type="text/css">
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { border: 0; display: block; }
p { margin: 0; padding: 0; }
</style>
</head>
<body style="margin:0; padding:0; background-color:#f2f2f2;">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="background-image: url('${DEFAULT_BACKGROUND_URL}'); background-size: cover; background-position: center; background-color:#f2f2f2;">
<tr>
<td align="center" style="padding:30px 16px;">
<table role="presentation" width="600" border="0" cellspacing="0" cellpadding="0" style="width:100%; max-width:600px; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
<tr>
<td style="padding:20px; font-family: Arial, sans-serif; color:#333333; word-break:break-word;">
<center>
<img src="${logoUrl}" alt="Port logo" width="100" style="margin-bottom:20px;" />
</center>
${headerHtml ? `<div>${headerHtml}</div>` : ''}
${body}
${footerHtml ? `<div style="margin-top:24px;">${footerHtml}</div>` : ''}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
/** Surface the brand primary color to template bodies. */
export function brandingPrimaryColor(branding?: BrandingShell | null): string {
return branding?.primaryColor ?? DEFAULT_PRIMARY_COLOR;
}