Files
pn-new-crm/src/lib/email/templates/crm-invite.ts

82 lines
2.9 KiB
TypeScript
Raw Normal View History

import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell';
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
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
interface InviteData {
link: string;
ttlHours: number;
recipientName?: string;
isSuperAdmin: boolean;
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
/** Display name for the port falls back to "Port Nimara" so the
* pre-multi-tenant default still reads correctly. */
portName?: string;
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
}
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
interface RenderOpts {
branding?: BrandingShell | null;
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
}
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
export function crmInviteEmail(
data: InviteData,
overrides?: RenderOpts,
): {
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
subject: string;
html: string;
text: string;
} {
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
const portName = data.portName ?? 'Port Nimara';
const subject = `You're invited to the ${portName} CRM`;
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
const greeting = data.recipientName ? `Dear ${escapeHtml(data.recipientName)},` : 'Welcome,';
const role = data.isSuperAdmin ? 'super administrator' : 'administrator';
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
const accent = brandingPrimaryColor(overrides?.branding);
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
const body = `
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
<p style="margin-bottom:10px; font-size:18px; font-weight:bold; color:${accent};">
Welcome to the ${escapeHtml(portName)} CRM
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
</p>
<p style="margin-bottom:10px; font-size:16px; line-height:1.5;">${greeting}</p>
<p style="margin-bottom:20px; font-size:16px; line-height:1.5;">
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
You've been invited to the ${escapeHtml(portName)} CRM as a ${role}. Click the
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
button below to set your password and activate your account. The
link expires in ${data.ttlHours} hours.
</p>
<p style="text-align:center; margin:30px 0;">
<a href="${safeUrl(data.link)}" style="display:inline-block; background-color:${accent}; color:#ffffff; text-decoration:none; padding:14px 35px; border-radius:5px; font-weight:bold; font-size:16px;">
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
Set up your account
</a>
</p>
<p style="font-size:14px; color:#666; line-height:1.5; padding:15px 0; border-top:1px solid #eee; margin-top:20px;">
If the button doesn't work, paste this link into your browser:<br />
<a href="${safeUrl(data.link)}" style="color:${accent}; text-decoration:underline; word-break:break-all;">${data.link}</a>
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
</p>
<p style="font-size:16px; margin-top:30px;">
Thank you,<br />
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
<strong>${escapeHtml(portName)} CRM</strong>
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
</p>`;
const text = [
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
`Welcome to the ${portName} CRM`,
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
'',
`You've been invited as a ${role}.`,
`Set up your account: ${data.link}`,
'',
`The link expires in ${data.ttlHours} hours.`,
'',
`Thank you,`,
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
`${portName} CRM`,
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
].join('\n');
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
return {
subject,
html: renderShell({ title: subject, body, branding: overrides?.branding }),
text,
};
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}