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 <ReservationList showBerth />. - 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:*.
This commit is contained in:
@@ -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({
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'companyName',
|
||||
accessorKey: 'companyName',
|
||||
header: 'Company',
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-muted-foreground">{(getValue() as string | null) ?? '—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'primaryContact',
|
||||
header: 'Primary Contact',
|
||||
@@ -82,6 +78,14 @@ export function getClientColumns({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nationality',
|
||||
accessorKey: 'nationality',
|
||||
header: 'Nationality',
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-muted-foreground">{(getValue() as string | null) ?? '—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'source',
|
||||
accessorKey: 'source',
|
||||
@@ -149,10 +153,7 @@ export function getClientColumns({
|
||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onArchive(row.original)}
|
||||
>
|
||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
|
||||
103
src/components/clients/client-companies-tab.tsx
Normal file
103
src/components/clients/client-companies-tab.tsx
Normal file
@@ -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 (
|
||||
<EmptyState
|
||||
title="No company memberships"
|
||||
description="This client is not affiliated with any companies yet. Add a membership from a company's detail page."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">Company affiliations</h3>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Company</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Primary</TableHead>
|
||||
<TableHead>Since</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{companies.map((m) => (
|
||||
<TableRow key={m.membershipId}>
|
||||
<TableCell>
|
||||
<Link
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
href={`/${portSlug}/companies/${m.company.id}` as any}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{m.company.name}
|
||||
</Link>
|
||||
{m.company.legalName && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
({m.company.legalName})
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize">{m.role.replace('_', ' ')}</TableCell>
|
||||
<TableCell>
|
||||
{m.isPrimary ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Primary
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{formatSince(m.startDate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<DetailLayout
|
||||
|
||||
@@ -24,11 +24,6 @@ export const clientFilterDefinitions: FilterDefinition[] = [
|
||||
type: 'text',
|
||||
placeholder: 'Filter by nationality...',
|
||||
},
|
||||
{
|
||||
key: 'isProxy',
|
||||
label: 'Proxy Client',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
key: 'includeArchived',
|
||||
label: 'Include Archived',
|
||||
|
||||
51
src/components/clients/client-reservations-tab.tsx
Normal file
51
src/components/clients/client-reservations-tab.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
|
||||
|
||||
interface ClientReservationsTabProps {
|
||||
clientId: string;
|
||||
activeReservations: Array<{
|
||||
id: string;
|
||||
berthId: string;
|
||||
yachtId: string;
|
||||
startDate: string | Date;
|
||||
tenureType: string;
|
||||
status: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Active reservations</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Showing currently active reservations. History is coming soon.
|
||||
</p>
|
||||
</div>
|
||||
<ReservationList
|
||||
reservations={rows}
|
||||
showBerth
|
||||
emptyMessage="This client has no active reservations."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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'] }) {
|
||||
<h3 className="text-sm font-medium mb-2">Personal Information</h3>
|
||||
<dl>
|
||||
<InfoRow label="Full Name" value={client.fullName} />
|
||||
<InfoRow label="Company" value={client.companyName} />
|
||||
<InfoRow label="Nationality" value={client.nationality} />
|
||||
<InfoRow label="Preferred Language" value={client.preferredLanguage} />
|
||||
<InfoRow label="Timezone" value={client.timezone} />
|
||||
<InfoRow
|
||||
label="Preferred Contact"
|
||||
value={client.preferredContactMethod}
|
||||
/>
|
||||
<InfoRow label="Preferred Contact" value={client.preferredContactMethod} />
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<span className="capitalize text-muted-foreground w-20 shrink-0">
|
||||
{c.channel}
|
||||
</span>
|
||||
<span className="capitalize text-muted-foreground w-20 shrink-0">{c.channel}</span>
|
||||
<span className="flex-1">{c.value}</span>
|
||||
{c.label && (
|
||||
<span className="text-xs text-muted-foreground capitalize">
|
||||
{c.label}
|
||||
</span>
|
||||
)}
|
||||
{c.isPrimary && (
|
||||
<span className="text-xs font-medium text-primary">Primary</span>
|
||||
<span className="text-xs text-muted-foreground capitalize">{c.label}</span>
|
||||
)}
|
||||
{c.isPrimary && <span className="text-xs font-medium text-primary">Primary</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -92,41 +106,6 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Yacht Details */}
|
||||
{(client.yachtName ||
|
||||
client.yachtLengthFt ||
|
||||
client.berthSizeDesired) && (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Yacht Details</h3>
|
||||
<dl>
|
||||
<InfoRow label="Yacht Name" value={client.yachtName} />
|
||||
<InfoRow
|
||||
label="Length"
|
||||
value={
|
||||
client.yachtLengthFt
|
||||
? `${client.yachtLengthFt} ft`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Width"
|
||||
value={
|
||||
client.yachtWidthFt ? `${client.yachtWidthFt} ft` : undefined
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Draft"
|
||||
value={
|
||||
client.yachtDraftFt
|
||||
? `${client.yachtDraftFt} ft`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<InfoRow label="Berth Size Desired" value={client.berthSizeDesired} />
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Source */}
|
||||
{(client.source || client.sourceDetails) && (
|
||||
<div className="space-y-1">
|
||||
@@ -138,34 +117,54 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Proxy Info */}
|
||||
{client.isProxy && (
|
||||
{/* Tags */}
|
||||
{client.tags && client.tags.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Proxy Information</h3>
|
||||
<dl>
|
||||
<InfoRow
|
||||
label="Proxy Type"
|
||||
value={client.proxyType?.replace('_', ' ')}
|
||||
/>
|
||||
<InfoRow label="Actual Owner" value={client.actualOwnerName} />
|
||||
</dl>
|
||||
<h3 className="text-sm font-medium mb-2">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{client.tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
style={{ backgroundColor: `${tag.color}20`, color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function getClientTabs({
|
||||
clientId,
|
||||
currentUserId,
|
||||
client,
|
||||
}: ClientTabsOptions): DetailTab[] {
|
||||
export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOptions): DetailTab[] {
|
||||
return [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
content: <OverviewTab client={client} />,
|
||||
},
|
||||
{
|
||||
id: 'yachts',
|
||||
label: 'Yachts',
|
||||
badge: client.yachts.length,
|
||||
content: <ClientYachtsTab clientId={clientId} yachts={client.yachts} />,
|
||||
},
|
||||
{
|
||||
id: 'companies',
|
||||
label: 'Companies',
|
||||
badge: client.companies.length,
|
||||
content: <ClientCompaniesTab clientId={clientId} companies={client.companies} />,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Reservations',
|
||||
badge: client.activeReservations.length,
|
||||
content: (
|
||||
<ClientReservationsTab clientId={clientId} activeReservations={client.activeReservations} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
label: 'Interests',
|
||||
@@ -178,13 +177,7 @@ export function getClientTabs({
|
||||
{
|
||||
id: 'notes',
|
||||
label: 'Notes',
|
||||
content: (
|
||||
<NotesList
|
||||
entityType="clients"
|
||||
entityId={clientId}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
),
|
||||
content: <NotesList entityType="clients" entityId={clientId} currentUserId={currentUserId} />,
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
|
||||
97
src/components/clients/client-yachts-tab.tsx
Normal file
97
src/components/clients/client-yachts-tab.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Client-owned yachts</h3>
|
||||
<PermissionGate resource="yachts" action="create">
|
||||
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="mr-1.5 h-4 w-4" />
|
||||
Add yacht
|
||||
</Button>
|
||||
</PermissionGate>
|
||||
</div>
|
||||
|
||||
{yachts.length === 0 ? (
|
||||
<EmptyState title="No yachts" description="No yachts owned by this client yet." />
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Dimensions</TableHead>
|
||||
<TableHead>Hull Number</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{yachts.map((y) => (
|
||||
<TableRow key={y.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
href={`/${portSlug}/yachts/${y.id}` as any}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{y.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{y.lengthFt && y.widthFt ? `${y.lengthFt} × ${y.widthFt} ft` : '—'}
|
||||
</TableCell>
|
||||
<TableCell>{y.hullNumber ?? '—'}</TableCell>
|
||||
<TableCell className="capitalize">{y.status.replace('_', ' ')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
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 && <YachtForm open={createOpen} onOpenChange={setCreateOpen} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user