Files
pn-new-crm/src/components/clients/client-detail.tsx
Matt 3e4d9d6310 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

122 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { DetailLayout } from '@/components/shared/detail-layout';
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
import { ClientDetailHeader } from '@/components/clients/client-detail-header';
import { getClientTabs } from '@/components/clients/client-tabs';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { useBreadcrumbHint } from '@/hooks/use-breadcrumb-hint';
import { apiFetch } from '@/lib/api/client';
import type { Address } from '@/components/shared/addresses-editor';
interface ClientData {
id: string;
portId: string;
fullName: string;
nationalityIso: string | null;
preferredContactMethod: string | null;
preferredLanguage: string | null;
timezone: string | null;
source: string | null;
sourceDetails: string | null;
archivedAt: string | null;
clientPortalEnabled: boolean;
createdAt: string;
updatedAt: string;
contacts: Array<{
id: string;
channel: string;
value: string;
valueE164: string | null;
valueCountry: string | null;
label: string | null;
isPrimary: boolean;
notes: string | null;
}>;
tags: Array<{
id: string;
name: string;
color: string;
}>;
yachts: Array<{
id: string;
name: string;
hullNumber: string | null;
registration: string | null;
lengthFt: string | null;
widthFt: string | null;
status: string;
}>;
companies: Array<{
membershipId: string;
role: string;
isPrimary: boolean;
startDate: string | Date;
company: {
id: string;
name: string;
legalName: string | null;
status: string;
};
}>;
activeReservations: Array<{
id: string;
berthId: string;
yachtId: string;
startDate: string | Date;
tenureType: string;
status: string;
}>;
addresses: Address[];
}
interface ClientDetailProps {
clientId: string;
currentUserId?: string;
}
export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) {
const { data, isLoading } = useQuery<ClientData>({
queryKey: ['clients', clientId],
queryFn: () =>
apiFetch<{ data: ClientData }>(`/api/v1/clients/${clientId}`).then((r) => r.data),
});
const { setChrome } = useMobileChrome();
const titleForChrome: string | null = data?.fullName ?? null;
useEffect(() => {
setChrome({ title: titleForChrome, showBackButton: true });
return () => setChrome({ title: null, showBackButton: false });
}, [titleForChrome, setChrome]);
// Topbar breadcrumb hint: replaces "Clients <uuid>" with
// "Clients Mary Smith". Hint clears on unmount.
useBreadcrumbHint(data ? { parents: [], current: data.fullName } : null);
useRealtimeInvalidation({
'client:updated': [['clients', clientId]],
'client:archived': [['clients', clientId]],
'client:restored': [['clients', clientId]],
'yacht:ownership_transferred': [['clients', clientId]],
'company_membership:added': [['clients', clientId]],
'company_membership:ended': [['clients', clientId]],
'berth_reservation:activated': [['clients', clientId]],
'berth_reservation:ended': [['clients', clientId]],
'berth_reservation:cancelled': [['clients', clientId]],
});
const tabs = data ? getClientTabs({ clientId, currentUserId, client: data }) : [];
return (
<DetailLayout
header={data ? <ClientDetailHeader client={data} /> : null}
tabs={tabs}
defaultTab="overview"
isLoading={isLoading}
/>
);
}