feat(post-audit): Phase 5 partial (4/8 templates) + 7.1 editor scaffold + per-entity reminder buttons

Phase 5 — luxury-port email tone (4 of 8 templates):
- portal-auth.tsx — activation + reset: "It's our pleasure to invite
  you to the {portName} client portal — your private space to review
  your berth, manage signed documents, and stay in touch with your
  sales liaison", sign-off "With warm regards, The {portName} Team",
  subjects "Welcome to {portName} — activate your client portal" /
  "Reset your {portName} portal password".
- inquiry-client-confirmation.tsx — "We've noted your enquiry, and a
  member of our team will be in touch shortly through your preferred
  channel", "should anything come to mind in the meantime", sign-off
  "With warm regards, The {portName} Sales Team".
- notification-digest.tsx — "Your {portName} update" header, "Here's
  what's waiting for you", "With warm regards, The {portName} Team".
- document-signing.tsx — all 4 sign-offs ("Dear X, ... Thank you, The
  {portName} team") rewritten to "With warm regards, The {portName} Team"
  with capitalised Team for consistency.
- Voice captured from old-CRM Nuxt repo
  (/Users/matt/Repos/Port Nimara/Port Nimara Client Portal/client-portal/
  server/utils/signature-notifications.ts) which already used "Dear",
  "Best regards", and collective sign-offs.

Remaining 4 templates (admin-email-change, crm-invite,
inquiry-sales-notification, residential-inquiry) + cross-port snapshot
tests queued as follow-up.

Phase 7.1 — PDF editor scaffold:
- New admin route /admin/templates/[id]/editor/page.tsx wired to a
  client-side <TemplateEditor>.
- Renders page 1 via react-pdf (worker URL pattern mirrors
  components/files/pdf-viewer.tsx); click-to-place markers in percent
  coordinates so a future page-size swap doesn't shift placements.
- Token picker over VALID_MERGE_TOKENS (sorted).
- Save persists overlayPositions via PATCH against the existing
  document_templates row; validator accepts the new field via
  fieldMapSchema from lib/templates/field-map.ts (no migration needed
  — overlay_positions JSONB column already exists).
- Outer/inner-body split + key-by-templateId remount avoids the
  in-render setState antipattern when seeding from server data.
- Add + delete markers supported. Multi-page, drag, resize, preview,
  new-PDF upload all defer to 7.2.

Per-entity polish:
- [+ Reminder] button on yacht / client / interest detail headers,
  threading defaultYachtId / defaultClientId / defaultInterestId so the
  ReminderForm opens with the entity pre-linked.
- [EOI] badge on yacht detail header when yacht.source === 'eoi-generated'
  (mirrors the contacts-editor pattern shipped in eaab149).

Phase 6 hardening:
- imap-bounce-poller strips whitespace from IMAP_PASS so Google
  Workspace App Passwords (16-char "abcd efgh ijkl mnop" format) work
  whether pasted with or without spaces. Confirmed via Google docs that
  the visual spaces are formatting only and must not reach the IMAP
  server.

Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 16:37:19 +02:00
parent eaab14943b
commit f938847ed9
12 changed files with 502 additions and 65 deletions

View File

@@ -32,12 +32,12 @@ function ClientConfirmationBody({
<>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>Dear {firstName},</Text>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
Thank you for expressing interest in {berthText}. Our team has registered your interest, and
we will reach out to you very shortly by your preferred method of contact with more
information.
Thank you for your interest in {berthText}. We&apos;ve noted your enquiry, and a member of
our team will be in touch shortly through your preferred channel with the details
you&apos;ve requested.
</Text>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
If you have any questions, please feel free to reach out to us at{' '}
Should anything come to mind in the meantime, please don&apos;t hesitate to write to us at{' '}
<Link
href={safeUrl(`mailto:${contactEmail}`)}
style={{ color: accent, textDecoration: 'underline' }}
@@ -47,7 +47,7 @@ function ClientConfirmationBody({
.
</Text>
<Text style={{ fontSize: '16px' }}>
Best regards,
With warm regards,
<br />
The {portName} Sales Team
</Text>
@@ -65,8 +65,8 @@ export async function inquiryClientConfirmation(
const subject = overrides?.subject?.trim()
? overrides.subject
: mooringNumber
? `Thank You for Your Interest in Berth ${mooringNumber}`
: `Thank You for Your Interest in a ${portName} Berth`;
? `Thank you for your interest in Berth ${mooringNumber}`
: `Thank you for your interest in ${portName}`;
const accent = brandingPrimaryColor(overrides?.branding);
const body = await render(
@@ -83,11 +83,11 @@ export async function inquiryClientConfirmation(
const text = [
`Dear ${firstName},`,
'',
`Thank you for expressing interest in ${berthText}. Our team has registered your interest, and we will reach out to you very shortly by your preferred method of contact with more information.`,
`Thank you for your interest in ${berthText}. We've noted your enquiry, and a member of our team will be in touch shortly through your preferred channel with the details you've requested.`,
'',
`If you have any questions, please feel free to reach out to us at ${contactEmail}.`,
`Should anything come to mind in the meantime, please don't hesitate to write to us at ${contactEmail}.`,
'',
'Best regards,',
'With warm regards,',
`The ${portName} Sales Team`,
].join('\n');