From b75834ab7e217796a748b184a340e308220a2dbe Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 14:36:34 +0200 Subject: [PATCH] refactor(clients): rebuild detail tabs + columns for new data model - ClientData in client-detail.tsx now reflects the stripped shape from Task 8.2 (drop companyName/isProxy/proxy*/yacht*/berthSizeDesired) and gains yachts / companies / activeReservations arrays. - client-tabs.tsx: Overview trimmed (personal, contacts, source, tags); three new count-badged tabs (Yachts, Companies, Reservations). - New client-yachts-tab.tsx renders owned yachts + Add yacht CTA (TODO: YachtForm preset-owner wiring for v2). - New client-companies-tab.tsx renders memberships with Primary badge and since-date; management still lives on the company detail page. - New client-reservations-tab.tsx maps activeReservations into ReservationRow shape and delegates to . - client-columns.tsx drops companyName column (TODO: add Yachts count + Primary company once list endpoint joins those). - client-filters.tsx drops isProxy filter. - Wire realtime invalidations for yacht:ownership_transferred, company_membership:added/ended, and berth_reservation:*. --- src/components/clients/client-columns.tsx | 27 ++-- .../clients/client-companies-tab.tsx | 103 ++++++++++++ src/components/clients/client-detail.tsx | 51 ++++-- src/components/clients/client-filters.tsx | 5 - .../clients/client-reservations-tab.tsx | 51 ++++++ src/components/clients/client-tabs.tsx | 153 +++++++++--------- src/components/clients/client-yachts-tab.tsx | 97 +++++++++++ 7 files changed, 374 insertions(+), 113 deletions(-) create mode 100644 src/components/clients/client-companies-tab.tsx create mode 100644 src/components/clients/client-reservations-tab.tsx create mode 100644 src/components/clients/client-yachts-tab.tsx diff --git a/src/components/clients/client-columns.tsx b/src/components/clients/client-columns.tsx index f53e5b1..e940370 100644 --- a/src/components/clients/client-columns.tsx +++ b/src/components/clients/client-columns.tsx @@ -18,7 +18,7 @@ import { TagBadge } from '@/components/shared/tag-badge'; export interface ClientRow { id: string; fullName: string; - companyName: string | null; + nationality: string | null; source: string | null; archivedAt: string | null; createdAt: string; @@ -39,6 +39,10 @@ interface GetColumnsOptions { onArchive: (client: ClientRow) => void; } +// TODO: Add "Yachts" (count) and "Primary company" columns once the +// GET /api/v1/clients list endpoint joins owned-yachts and primary-company +// data into the row shape. Until then, the columns are omitted rather than +// shown as empty placeholders. export function getClientColumns({ portSlug, onEdit, @@ -59,14 +63,6 @@ export function getClientColumns({ ), }, - { - id: 'companyName', - accessorKey: 'companyName', - header: 'Company', - cell: ({ getValue }) => ( - {(getValue() as string | null) ?? '—'} - ), - }, { id: 'primaryContact', header: 'Primary Contact', @@ -82,6 +78,14 @@ export function getClientColumns({ ); }, }, + { + id: 'nationality', + accessorKey: 'nationality', + header: 'Nationality', + cell: ({ getValue }) => ( + {(getValue() as string | null) ?? '—'} + ), + }, { id: 'source', accessorKey: 'source', @@ -149,10 +153,7 @@ export function getClientColumns({ Edit - onArchive(row.original)} - > + onArchive(row.original)}> Archive diff --git a/src/components/clients/client-companies-tab.tsx b/src/components/clients/client-companies-tab.tsx new file mode 100644 index 0000000..80403dd --- /dev/null +++ b/src/components/clients/client-companies-tab.tsx @@ -0,0 +1,103 @@ +'use client'; + +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import { format } from 'date-fns'; + +import { + Table, + TableHeader, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { EmptyState } from '@/components/shared/empty-state'; + +interface ClientCompaniesTabProps { + clientId: string; + companies: Array<{ + membershipId: string; + role: string; + isPrimary: boolean; + startDate: string | Date; + company: { + id: string; + name: string; + legalName: string | null; + status: string; + }; + }>; +} + +function formatSince(startDate: string | Date): string { + const d = typeof startDate === 'string' ? new Date(startDate) : startDate; + if (Number.isNaN(d.getTime())) return '—'; + return format(d, 'MMM d, yyyy'); +} + +export function ClientCompaniesTab({ clientId: _clientId, companies }: ClientCompaniesTabProps) { + const routeParams = useParams<{ portSlug: string }>(); + const portSlug = routeParams?.portSlug ?? ''; + + if (companies.length === 0) { + return ( + + ); + } + + return ( +
+

Company affiliations

+
+ + + + Company + Role + Primary + Since + + + + {companies.map((m) => ( + + + + {m.company.name} + + {m.company.legalName && ( + + ({m.company.legalName}) + + )} + + {m.role.replace('_', ' ')} + + {m.isPrimary ? ( + + Primary + + ) : ( + + )} + + + {formatSince(m.startDate)} + + + ))} + +
+
+
+ ); +} diff --git a/src/components/clients/client-detail.tsx b/src/components/clients/client-detail.tsx index 6e7b178..bb15697 100644 --- a/src/components/clients/client-detail.tsx +++ b/src/components/clients/client-detail.tsx @@ -12,19 +12,7 @@ interface ClientData { id: string; portId: string; fullName: string; - companyName: string | null; nationality: string | null; - isProxy: boolean; - proxyType: string | null; - actualOwnerName: string | null; - yachtName: string | null; - yachtLengthFt: string | null; - yachtWidthFt: string | null; - yachtDraftFt: string | null; - yachtLengthM: string | null; - yachtWidthM: string | null; - yachtDraftM: string | null; - berthSizeDesired: string | null; preferredContactMethod: string | null; preferredLanguage: string | null; timezone: string | null; @@ -46,6 +34,35 @@ interface ClientData { 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; + }>; } interface ClientDetailProps { @@ -64,11 +81,15 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) { '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 }) - : []; + const tabs = data ? getClientTabs({ clientId, currentUserId, client: data }) : []; return ( ; +} + +export function ClientReservationsTab({ + clientId, + activeReservations, +}: ClientReservationsTabProps) { + const rows: ReservationRow[] = activeReservations.map((r) => ({ + id: r.id, + berthId: r.berthId, + portId: '', // not rendered by ReservationList + clientId, + yachtId: r.yachtId, + status: r.status as ReservationRow['status'], + startDate: typeof r.startDate === 'string' ? r.startDate : r.startDate.toISOString(), + endDate: null, + tenureType: r.tenureType, + contractFileId: null, + notes: null, + createdAt: '', + })); + + return ( +
+
+

Active reservations

+

+ Showing currently active reservations. History is coming soon. +

+
+ +
+ ); +} diff --git a/src/components/clients/client-tabs.tsx b/src/components/clients/client-tabs.tsx index 9c0254b..9537b12 100644 --- a/src/components/clients/client-tabs.tsx +++ b/src/components/clients/client-tabs.tsx @@ -2,22 +2,16 @@ import type { DetailTab } from '@/components/shared/detail-layout'; import { NotesList } from '@/components/shared/notes-list'; +import { ClientYachtsTab } from '@/components/clients/client-yachts-tab'; +import { ClientCompaniesTab } from '@/components/clients/client-companies-tab'; +import { ClientReservationsTab } from '@/components/clients/client-reservations-tab'; interface ClientTabsOptions { clientId: string; currentUserId?: string; client: { fullName: string; - companyName?: string | null; nationality?: string | null; - isProxy?: boolean; - proxyType?: string | null; - actualOwnerName?: string | null; - yachtName?: string | null; - yachtLengthFt?: string | null; - yachtWidthFt?: string | null; - yachtDraftFt?: string | null; - berthSizeDesired?: string | null; preferredContactMethod?: string | null; preferredLanguage?: string | null; timezone?: string | null; @@ -30,6 +24,36 @@ interface ClientTabsOptions { label?: string | null; isPrimary: boolean; }>; + 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; + }>; + tags?: Array<{ id: string; name: string; color: string }>; }; } @@ -51,14 +75,10 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {

Personal Information

- - +
@@ -72,18 +92,12 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) { key={c.id} className="flex items-center gap-2 p-2 rounded-lg border bg-muted/30 text-sm" > - - {c.channel} - + {c.channel} {c.value} {c.label && ( - - {c.label} - - )} - {c.isPrimary && ( - Primary + {c.label} )} + {c.isPrimary && Primary} ))} @@ -92,41 +106,6 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) { )} - {/* Yacht Details */} - {(client.yachtName || - client.yachtLengthFt || - client.berthSizeDesired) && ( -
-

Yacht Details

-
- - - - - -
-
- )} - {/* Source */} {(client.source || client.sourceDetails) && (
@@ -138,34 +117,54 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
)} - {/* Proxy Info */} - {client.isProxy && ( + {/* Tags */} + {client.tags && client.tags.length > 0 && (
-

Proxy Information

-
- - -
+

Tags

+
+ {client.tags.map((tag) => ( + + {tag.name} + + ))} +
)} ); } -export function getClientTabs({ - clientId, - currentUserId, - client, -}: ClientTabsOptions): DetailTab[] { +export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOptions): DetailTab[] { return [ { id: 'overview', label: 'Overview', content: , }, + { + id: 'yachts', + label: 'Yachts', + badge: client.yachts.length, + content: , + }, + { + id: 'companies', + label: 'Companies', + badge: client.companies.length, + content: , + }, + { + id: 'reservations', + label: 'Reservations', + badge: client.activeReservations.length, + content: ( + + ), + }, { id: 'interests', label: 'Interests', @@ -178,13 +177,7 @@ export function getClientTabs({ { id: 'notes', label: 'Notes', - content: ( - - ), + content: , }, { id: 'files', diff --git a/src/components/clients/client-yachts-tab.tsx b/src/components/clients/client-yachts-tab.tsx new file mode 100644 index 0000000..c724e32 --- /dev/null +++ b/src/components/clients/client-yachts-tab.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import { Plus } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + Table, + TableHeader, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@/components/ui/table'; +import { EmptyState } from '@/components/shared/empty-state'; +import { PermissionGate } from '@/components/shared/permission-gate'; +import { YachtForm } from '@/components/yachts/yacht-form'; + +interface ClientYachtsTabProps { + clientId: string; + yachts: Array<{ + id: string; + name: string; + hullNumber: string | null; + registration: string | null; + lengthFt: string | null; + widthFt: string | null; + status: string; + }>; +} + +export function ClientYachtsTab({ clientId: _clientId, yachts }: ClientYachtsTabProps) { + const routeParams = useParams<{ portSlug: string }>(); + const portSlug = routeParams?.portSlug ?? ''; + const [createOpen, setCreateOpen] = useState(false); + + return ( +
+
+

Client-owned yachts

+ + + +
+ + {yachts.length === 0 ? ( + + ) : ( +
+ + + + Name + Dimensions + Hull Number + Status + + + + {yachts.map((y) => ( + + + + {y.name} + + + + {y.lengthFt && y.widthFt ? `${y.lengthFt} × ${y.widthFt} ft` : '—'} + + {y.hullNumber ?? '—'} + {y.status.replace('_', ' ')} + + ))} + +
+
+ )} + + {/* + TODO: YachtForm (Task 5.2) does not yet accept a preset owner prop. + When opened here, the user must manually pick this client in the owner + picker. Wire an `initialOwner` prop into YachtForm in a follow-up so + we can pre-select `{ type: 'client', id: clientId }`. + */} + {createOpen && } +
+ ); +}