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:
@@ -15,10 +15,16 @@ import type { CountryCode } from '@/lib/i18n/countries';
|
||||
|
||||
interface PrefillData {
|
||||
token: { expiresAt: string; consumed: boolean };
|
||||
port: {
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
backgroundUrl: string | null;
|
||||
};
|
||||
client: {
|
||||
fullName: string;
|
||||
streetAddress: string | null;
|
||||
city: string | null;
|
||||
subdivisionIso: string | null;
|
||||
postalCode: string | null;
|
||||
country: string | null;
|
||||
primaryEmail: string | null;
|
||||
@@ -48,6 +54,9 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
// Form fields
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [city, setCity] = useState('');
|
||||
const [region, setRegion] = useState('');
|
||||
const [postalCode, setPostalCode] = useState('');
|
||||
const [country, setCountry] = useState<CountryCode | null>(null);
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState<PhoneInputValue | null>(null);
|
||||
@@ -74,6 +83,9 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
setData(payload.data);
|
||||
setFullName(payload.data.client.fullName ?? '');
|
||||
setAddress(payload.data.client.streetAddress ?? '');
|
||||
setCity(payload.data.client.city ?? '');
|
||||
setRegion(payload.data.client.subdivisionIso ?? '');
|
||||
setPostalCode(payload.data.client.postalCode ?? '');
|
||||
setCountry((payload.data.client.country as CountryCode | null) ?? null);
|
||||
setEmail(payload.data.client.primaryEmail ?? '');
|
||||
if (payload.data.client.primaryPhone) {
|
||||
@@ -114,6 +126,9 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
body: JSON.stringify({
|
||||
fullName: fullName.trim(),
|
||||
address: address.trim() || null,
|
||||
city: city.trim() || null,
|
||||
subdivisionIso: region.trim() || null,
|
||||
postalCode: postalCode.trim() || null,
|
||||
country: country ?? null,
|
||||
email: email.trim() || null,
|
||||
phoneE164: phone?.e164 ?? null,
|
||||
@@ -137,9 +152,20 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Branding prop fed into BrandedAuthShell. Falls back to nulls until
|
||||
// `data` lands; once it lands the shell re-renders with the resolved
|
||||
// port logo + backdrop.
|
||||
const branding = data
|
||||
? {
|
||||
logoUrl: data.port.logoUrl,
|
||||
backgroundUrl: data.port.backgroundUrl,
|
||||
appName: data.port.name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<BrandedAuthShell width="md" branding={branding}>
|
||||
<div role="status" aria-live="polite" className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="sr-only">Loading</span>
|
||||
@@ -150,7 +176,7 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<BrandedAuthShell width="md" branding={branding}>
|
||||
<div role="alert" aria-live="assertive" className="text-center space-y-2 py-6">
|
||||
<p className="text-sm text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
@@ -158,19 +184,15 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Tokens are now reusable until expiry - the consumed flag is kept
|
||||
// so the form can show a soft "you've submitted this before" banner
|
||||
// (and prefill the entered values) without locking the recipient out
|
||||
// of updating their details.
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<BrandedAuthShell width="md" branding={branding}>
|
||||
<div className="text-center space-y-3 py-6">
|
||||
<CheckCircle2 className="h-10 w-10 text-emerald-600 mx-auto" />
|
||||
<h1 className="text-lg font-semibold">Thanks, got it</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your details have been sent to the team. Watch your inbox for your EOI document shortly.
|
||||
Your details have been sent to the team at {data?.port.name ?? 'the marina'}. Watch your
|
||||
inbox for your EOI document shortly.
|
||||
</p>
|
||||
</div>
|
||||
</BrandedAuthShell>
|
||||
@@ -178,13 +200,17 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<BrandedAuthShell width="md" branding={branding}>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-1 text-center">
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{data?.port.name}
|
||||
</p>
|
||||
<h1 className="text-xl font-semibold">A few details before we draft your EOI</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We've pre-filled what we have on file. Please review, correct anything that's
|
||||
wrong, and add what's missing.
|
||||
wrong, and add what's missing. Submissions go straight to the team handling your
|
||||
application.
|
||||
</p>
|
||||
{data?.token.consumed ? (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
||||
@@ -227,19 +253,45 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="address">Address</Label>
|
||||
<Label htmlFor="address">Street address</Label>
|
||||
<Textarea
|
||||
id="address"
|
||||
rows={2}
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
placeholder="Street, city, postal code"
|
||||
placeholder="Apartment, suite, building, street"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Country</Label>
|
||||
<CountryCombobox value={country} onChange={(c) => setCountry(c ?? null)} clearable />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="city">City</Label>
|
||||
<Input id="city" value={city} onChange={(e) => setCity(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="postalCode">Postal code</Label>
|
||||
<Input
|
||||
id="postalCode"
|
||||
value={postalCode}
|
||||
onChange={(e) => setPostalCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="region">Region / state</Label>
|
||||
<Input
|
||||
id="region"
|
||||
value={region}
|
||||
onChange={(e) => setRegion(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Country</Label>
|
||||
<CountryCombobox value={country} onChange={(c) => setCountry(c ?? null)} clearable />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -299,7 +351,7 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
This link is private to you and expires after one use.
|
||||
This link is private to you. You can come back and update your details until it expires.
|
||||
</p>
|
||||
</form>
|
||||
</BrandedAuthShell>
|
||||
|
||||
Reference in New Issue
Block a user