feat(launch-readiness-batch): UAT drains, navigation refactor, launch infra, trackers
Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.
UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
Aggregated helpers in notes.service mirror the listFor*Aggregated
symmetric-reach joins. yacht-tabs + company-tabs render the
badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
`width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
fixed inset-0 pin so long forms scroll naturally). Form picks up
port branding (logoUrl + backgroundUrl + appName) via
loadByToken. Address fields completed (street + city + region +
postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
emits toast.success with action link to the destination entity
or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
(5 types visible at once).
Launch infra:
- UTM column wiring (Init 1b step 4): migration
0089_website_submissions_utm.sql adds utm_source/medium/campaign/
term/content + composite index (port_id, utm_source, received_at)
for per-campaign rollups. website-inquiries intake accepts the
five fields. Residential intake intentionally untouched per audit
scope.
- Invoicing module gate (Init 1c spike): new
invoices-module.service + invoices layout guard + registry entry
invoices_module_enabled (default false). Audit conclusion in
launch-readiness.md: payments table is canonical money path;
/invoices flow is parallel infrastructure now hidden by default.
Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
New route-labels.ts + use-smart-back hook +
navigation-history-tracker so back falls through to the parent
route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
breadcrumb-store kept for back-compat consumers but the
breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
upload-receipts, reports kind, tenancies detail, analytics
metric, client detail) migrated.
Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
the reports gap audit (cross-cutting filter set, Marketing +
Financial blockers, custom builder remaining entities, scheduled
CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
(each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
* adding an entry surfaces it without any UI change.
|
||||
*/
|
||||
|
||||
import type { BrandingShell } from '@/lib/email/shell';
|
||||
import { activationEmail, resetEmail } from '@/lib/email/templates/portal-auth';
|
||||
import { crmInviteEmail } from '@/lib/email/templates/crm-invite';
|
||||
import { adminEmailChangeEmail } from '@/lib/email/templates/admin-email-change';
|
||||
@@ -52,6 +53,11 @@ export interface SampleContext {
|
||||
recipientEmail: string;
|
||||
portName: string;
|
||||
portUrl: string;
|
||||
/** Per-port branding shell (logo, blur background, accent color, header/footer
|
||||
* HTML). Resolved once by the test-template route via getBrandingShell and
|
||||
* forwarded into every template so previews match the production look.
|
||||
* Null is acceptable - templates fall back to neutral defaults. */
|
||||
branding: BrandingShell | null;
|
||||
}
|
||||
|
||||
export const TEST_TEMPLATES: TestTemplateMeta[] = [
|
||||
@@ -60,185 +66,224 @@ export const TEST_TEMPLATES: TestTemplateMeta[] = [
|
||||
label: 'Portal · Activation invite',
|
||||
description: 'Fires when an admin invites a client to activate their portal account.',
|
||||
render: (s) =>
|
||||
activationEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
link: `${s.portUrl}/portal/activate/sample-token`,
|
||||
ttlHours: 24,
|
||||
}),
|
||||
activationEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
link: `${s.portUrl}/portal/activate/sample-token`,
|
||||
ttlHours: 24,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'portal_reset',
|
||||
label: 'Portal · Password reset',
|
||||
description: 'Fires when a portal user requests a password reset link.',
|
||||
render: (s) =>
|
||||
resetEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
link: `${s.portUrl}/portal/reset/sample-token`,
|
||||
ttlMinutes: 120,
|
||||
}),
|
||||
resetEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
link: `${s.portUrl}/portal/reset/sample-token`,
|
||||
ttlMinutes: 120,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'crm_invite',
|
||||
label: 'CRM · Teammate invitation',
|
||||
description: 'Fires when a super-admin invites a new teammate to the CRM.',
|
||||
render: (s) =>
|
||||
crmInviteEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
isSuperAdmin: false,
|
||||
link: `${s.portUrl}/invite/sample-token`,
|
||||
ttlHours: 72,
|
||||
}),
|
||||
crmInviteEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
isSuperAdmin: false,
|
||||
link: `${s.portUrl}/invite/sample-token`,
|
||||
ttlHours: 72,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'admin_email_change',
|
||||
label: 'CRM · Admin email change confirmation',
|
||||
description: 'Fires when an admin updates their CRM login email - confirmation step.',
|
||||
render: (s) =>
|
||||
adminEmailChangeEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
newEmail: s.recipientEmail,
|
||||
changedByDisplayName: 'Sample Admin',
|
||||
loginUrl: `${s.portUrl}/login`,
|
||||
}),
|
||||
adminEmailChangeEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
newEmail: s.recipientEmail,
|
||||
changedByDisplayName: 'Sample Admin',
|
||||
loginUrl: `${s.portUrl}/login`,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'notification_digest',
|
||||
label: 'Reminders · Notification digest',
|
||||
description: 'Fires on the configured cadence (daily/weekly) with the rep’s open reminders.',
|
||||
render: (s) =>
|
||||
notificationDigestEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
items: [
|
||||
{
|
||||
type: 'reminder',
|
||||
title: 'Follow up with Matthew Ciaccio on Berth A1',
|
||||
description: 'Reservation EOI sent 5 days ago - no response yet.',
|
||||
link: `${s.portUrl}/clients/sample-client-id`,
|
||||
createdAt: new Date(Date.now() - 86_400_000),
|
||||
},
|
||||
{
|
||||
type: 'alert',
|
||||
title: 'Berth B12 PDF parse failed',
|
||||
description: null,
|
||||
link: `${s.portUrl}/berths/sample-berth-id`,
|
||||
createdAt: new Date(Date.now() - 2 * 86_400_000),
|
||||
},
|
||||
],
|
||||
totalUnread: 2,
|
||||
inboxLink: `${s.portUrl}/inbox`,
|
||||
}),
|
||||
notificationDigestEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
items: [
|
||||
{
|
||||
type: 'reminder',
|
||||
title: 'Follow up with Matthew Ciaccio on Berth A1',
|
||||
description: 'Reservation EOI sent 5 days ago - no response yet.',
|
||||
link: `${s.portUrl}/clients/sample-client-id`,
|
||||
createdAt: new Date(Date.now() - 86_400_000),
|
||||
},
|
||||
{
|
||||
type: 'alert',
|
||||
title: 'Berth B12 PDF parse failed',
|
||||
description: null,
|
||||
link: `${s.portUrl}/berths/sample-berth-id`,
|
||||
createdAt: new Date(Date.now() - 2 * 86_400_000),
|
||||
},
|
||||
],
|
||||
totalUnread: 2,
|
||||
inboxLink: `${s.portUrl}/inbox`,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'signing_invitation',
|
||||
label: 'Documenso · Signing invitation',
|
||||
description: 'Fires when the rep dispatches the first signing-invite email for a doc.',
|
||||
render: (s) =>
|
||||
signingInvitationEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
signerRole: 'client',
|
||||
signingUrl: `${s.portUrl}/sign/sample-token`,
|
||||
senderName: 'Sample Sales Manager',
|
||||
customMessage: null,
|
||||
}),
|
||||
signingInvitationEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
signerRole: 'client',
|
||||
signingUrl: `${s.portUrl}/sign/sample-token`,
|
||||
senderName: 'Sample Sales Manager',
|
||||
customMessage: null,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'signing_reminder',
|
||||
label: 'Documenso · Signing reminder',
|
||||
description: 'Fires when a manual reminder is dispatched for an outstanding signer.',
|
||||
render: (s) =>
|
||||
signingReminderEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
signingUrl: `${s.portUrl}/sign/sample-token`,
|
||||
invitedAgo: '5 days ago',
|
||||
customMessage: null,
|
||||
}),
|
||||
signingReminderEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
signingUrl: `${s.portUrl}/sign/sample-token`,
|
||||
invitedAgo: '5 days ago',
|
||||
customMessage: null,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'signing_completed',
|
||||
label: 'Documenso · Fully signed notification',
|
||||
description: 'Fires when every required signer has signed and the document is complete.',
|
||||
render: (s) =>
|
||||
signingCompletedEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
clientName: s.recipientName,
|
||||
completedAt: new Date(),
|
||||
}),
|
||||
signingCompletedEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
clientName: s.recipientName,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'signing_cancelled',
|
||||
label: 'Documenso · Signing cancelled',
|
||||
description: 'Fires when the rep cancels a document mid-signature with notify-recipients.',
|
||||
render: (s) =>
|
||||
signingCancelledEmail({
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
reason: 'Customer renegotiated terms; a fresh contract will follow.',
|
||||
}),
|
||||
signingCancelledEmail(
|
||||
{
|
||||
recipientName: s.recipientName,
|
||||
portName: s.portName,
|
||||
documentLabel: 'Sales Contract',
|
||||
reason: 'Customer renegotiated terms; a fresh contract will follow.',
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'inquiry_client_confirmation',
|
||||
label: 'Public inquiry · Client confirmation',
|
||||
description: 'Fires when a public-site visitor submits the contact form (their copy).',
|
||||
render: (s) =>
|
||||
inquiryClientConfirmation({
|
||||
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
||||
mooringNumber: 'A1',
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: s.portName,
|
||||
}),
|
||||
inquiryClientConfirmation(
|
||||
{
|
||||
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
||||
mooringNumber: 'A1',
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: s.portName,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'inquiry_sales_notification',
|
||||
label: 'Public inquiry · Sales notification',
|
||||
description: 'Fires alongside the client confirmation - alerts the sales rep to a new lead.',
|
||||
render: (s) =>
|
||||
inquirySalesNotification({
|
||||
fullName: s.recipientName,
|
||||
email: s.recipientEmail,
|
||||
phone: '+1 555 0100',
|
||||
mooringNumber: 'A1',
|
||||
crmUrl: `${s.portUrl}/clients/sample-client-id`,
|
||||
portName: s.portName,
|
||||
}),
|
||||
inquirySalesNotification(
|
||||
{
|
||||
fullName: s.recipientName,
|
||||
email: s.recipientEmail,
|
||||
phone: '+1 555 0100',
|
||||
mooringNumber: 'A1',
|
||||
crmUrl: `${s.portUrl}/clients/sample-client-id`,
|
||||
portName: s.portName,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'residential_client_confirmation',
|
||||
label: 'Residential inquiry · Client confirmation',
|
||||
description: 'Fires when a residential-site visitor submits the contact form.',
|
||||
render: (s) =>
|
||||
residentialClientConfirmation({
|
||||
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: s.portName,
|
||||
}),
|
||||
residentialClientConfirmation(
|
||||
{
|
||||
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: s.portName,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'residential_sales_alert',
|
||||
label: 'Residential inquiry · Sales alert',
|
||||
description: 'Fires alongside the residential client confirmation - alerts the sales team.',
|
||||
render: (s) =>
|
||||
residentialSalesAlert({
|
||||
fullName: s.recipientName,
|
||||
email: s.recipientEmail,
|
||||
phone: '+1 555 0100',
|
||||
placeOfResidence: 'Monaco',
|
||||
preferredContactMethod: 'email',
|
||||
notes: 'Looking for year-round mooring + marina apartment access.',
|
||||
crmDeepLink: `${s.portUrl}/residential/clients/sample-id`,
|
||||
portName: s.portName,
|
||||
}),
|
||||
residentialSalesAlert(
|
||||
{
|
||||
fullName: s.recipientName,
|
||||
email: s.recipientEmail,
|
||||
phone: '+1 555 0100',
|
||||
placeOfResidence: 'Monaco',
|
||||
preferredContactMethod: 'email',
|
||||
notes: 'Looking for year-round mooring + marina apartment access.',
|
||||
crmDeepLink: `${s.portUrl}/residential/clients/sample-id`,
|
||||
portName: s.portName,
|
||||
},
|
||||
{ branding: s.branding },
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user