Files
pn-new-crm/src/components/yachts/yacht-detail.tsx

105 lines
3.2 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { DetailLayout } from '@/components/shared/detail-layout';
import { DetailNotFound } from '@/components/shared/detail-not-found';
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
import { YachtDetailHeader } from '@/components/yachts/yacht-detail-header';
import { getYachtTabs } from '@/components/yachts/yacht-tabs';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul Major interest workflow expansion driven by the rapid-fire UX session. EOI / Contract / Reservation tabs replace the generic Documents tab when the deal is at the relevant stage — workspace pattern with active-doc hero, signing progress, paper-signed upload, and history strip. Stage- conditional visibility wired through interest-tabs.tsx so the tab set shrinks/expands as the deal moves through the pipeline. Contact log: per-interaction structured log (channel/direction/summary/ optional follow-up reminder). New `interest_contact_log` table + service + tab UI (timeline with channel-coded icons + compose dialog). auto-creates a reminder when followUpAt is set. Berth Interest milestone: first milestone in the OverviewTab's pipeline strip, completes the moment any berth is linked via the junction. Drives the "have we captured what they want?" sanity check for general_interest leads before they move to EOI. Stage-conditional milestones: past phases collapse into a one-liner strip, current phase expands, future phases hide behind a "Show upcoming" toggle. Inline stage picker now defers reason capture to an override-confirm view (only required for illegal transitions, not the default flow). Notes blob → threaded: dropped `interests.notes` column entirely; the threaded `interest_notes` table is the single source of truth. Latest- note teaser on Overview links into the dedicated Notes tab. Polymorphic notes service gains aggregated client view (unions client + interest + yacht notes with source chips and group-by-source toggle). Berth interest list overhaul: - Configurable columns via ColumnPicker (18 toggleable, 5 default-on) - Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2) - Per-letter row tinting via colored left-border accent + dot in cell - Documents tab merged Files (single attachments section) Topbar improvements: - Always-visible back arrow on detail pages (path depth > 2) - Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can push their entity hierarchy (Clients › Mary Smith › Interest › B17) - Tighter spacing, softer separators, 160px crumb truncation DataTable upgrades: - Page-size selector with All option (validator cap raised to 1000) - getRowClassName slot for per-row styling (used by berth tinting) - Fixed Radix SelectItem crash on empty-string values via __any__ sentinel (was crashing every list page that opened a select filter) Interest list: - Configurable columns picker - Stage cell clickable into detail - TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons - Save view moved into ColumnPicker menu; Views button hidden when no views are saved - Pipeline kanban board endpoint at /api/v1/interests/board with minimal projection, 5000-row cap + truncated banner, filter pass-through Mobile chrome + sidebar collapse removed (always-expanded design choice). User management lists super-admins (was inner-joined on user_port_roles which excluded global super-admins). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
import { useBreadcrumbHint } from '@/hooks/use-breadcrumb-hint';
import { apiFetch } from '@/lib/api/client';
export interface YachtData {
id: string;
portId: string;
name: string;
hullNumber: string | null;
registration: string | null;
flag: string | null;
yearBuilt: number | null;
builder: string | null;
model: string | null;
hullMaterial: string | null;
lengthFt: string | null;
widthFt: string | null;
draftFt: string | null;
lengthM: string | null;
widthM: string | null;
draftM: string | null;
currentOwnerType: 'client' | 'company';
currentOwnerId: string;
status: string;
notes: string | null;
archivedAt: string | null;
createdAt: string;
updatedAt: string;
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>
2026-05-18 16:37:19 +02:00
/** Phase 3c — origin tracking. Surfaces the [EOI] badge in the header. */
source?: string | null;
sourceDocumentId?: string | null;
}
interface YachtDetailProps {
yachtId: string;
currentUserId?: string;
}
export function YachtDetail({ yachtId, currentUserId }: YachtDetailProps) {
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const { data, isLoading, error } = useQuery<YachtData>({
queryKey: ['yachts', yachtId],
queryFn: () => apiFetch<{ data: YachtData }>(`/api/v1/yachts/${yachtId}`).then((r) => r.data),
retry: (failureCount, err) => {
const status = (err as { status?: number } | null | undefined)?.status;
if (status === 404 || status === 403) return false;
return failureCount < 2;
},
});
const { setChrome } = useMobileChrome();
const titleForChrome: string | null = data?.name ?? null;
useEffect(() => {
setChrome({ title: titleForChrome, showBackButton: true });
return () => setChrome({ title: null, showBackButton: false });
}, [titleForChrome, setChrome]);
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul Major interest workflow expansion driven by the rapid-fire UX session. EOI / Contract / Reservation tabs replace the generic Documents tab when the deal is at the relevant stage — workspace pattern with active-doc hero, signing progress, paper-signed upload, and history strip. Stage- conditional visibility wired through interest-tabs.tsx so the tab set shrinks/expands as the deal moves through the pipeline. Contact log: per-interaction structured log (channel/direction/summary/ optional follow-up reminder). New `interest_contact_log` table + service + tab UI (timeline with channel-coded icons + compose dialog). auto-creates a reminder when followUpAt is set. Berth Interest milestone: first milestone in the OverviewTab's pipeline strip, completes the moment any berth is linked via the junction. Drives the "have we captured what they want?" sanity check for general_interest leads before they move to EOI. Stage-conditional milestones: past phases collapse into a one-liner strip, current phase expands, future phases hide behind a "Show upcoming" toggle. Inline stage picker now defers reason capture to an override-confirm view (only required for illegal transitions, not the default flow). Notes blob → threaded: dropped `interests.notes` column entirely; the threaded `interest_notes` table is the single source of truth. Latest- note teaser on Overview links into the dedicated Notes tab. Polymorphic notes service gains aggregated client view (unions client + interest + yacht notes with source chips and group-by-source toggle). Berth interest list overhaul: - Configurable columns via ColumnPicker (18 toggleable, 5 default-on) - Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2) - Per-letter row tinting via colored left-border accent + dot in cell - Documents tab merged Files (single attachments section) Topbar improvements: - Always-visible back arrow on detail pages (path depth > 2) - Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can push their entity hierarchy (Clients › Mary Smith › Interest › B17) - Tighter spacing, softer separators, 160px crumb truncation DataTable upgrades: - Page-size selector with All option (validator cap raised to 1000) - getRowClassName slot for per-row styling (used by berth tinting) - Fixed Radix SelectItem crash on empty-string values via __any__ sentinel (was crashing every list page that opened a select filter) Interest list: - Configurable columns picker - Stage cell clickable into detail - TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons - Save view moved into ColumnPicker menu; Views button hidden when no views are saved - Pipeline kanban board endpoint at /api/v1/interests/board with minimal projection, 5000-row cap + truncated banner, filter pass-through Mobile chrome + sidebar collapse removed (always-expanded design choice). User management lists super-admins (was inner-joined on user_port_roles which excluded global super-admins). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
useBreadcrumbHint(data ? { parents: [], current: data.name } : null);
useRealtimeInvalidation({
'yacht:updated': [['yachts', yachtId]],
'yacht:archived': [['yachts', yachtId]],
'yacht:ownership_transferred': [
['yachts', yachtId],
['yachts', yachtId, 'ownership-history'],
],
});
if (error && !isLoading) {
const status = (error as { status?: number } | null | undefined)?.status;
return (
<DetailNotFound
entity="yacht"
backHref={`/${portSlug}/yachts`}
backLabel="Back to yachts"
status={status}
/>
);
}
const tabs = data ? getYachtTabs({ yachtId, currentUserId, yacht: data }) : [];
return (
<DetailLayout
header={data ? <YachtDetailHeader yacht={data} /> : null}
tabs={tabs}
defaultTab="overview"
isLoading={isLoading}
/>
);
}