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 {
|
export interface ClientRow {
|
||||||
id: string;
|
id: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
companyName: string | null;
|
nationality: string | null;
|
||||||
source: string | null;
|
source: string | null;
|
||||||
archivedAt: string | null;
|
archivedAt: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -39,6 +39,10 @@ interface GetColumnsOptions {
|
|||||||
onArchive: (client: ClientRow) => void;
|
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({
|
export function getClientColumns({
|
||||||
portSlug,
|
portSlug,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -59,14 +63,6 @@ export function getClientColumns({
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'companyName',
|
|
||||||
accessorKey: 'companyName',
|
|
||||||
header: 'Company',
|
|
||||||
cell: ({ getValue }) => (
|
|
||||||
<span className="text-muted-foreground">{(getValue() as string | null) ?? '—'}</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'primaryContact',
|
id: 'primaryContact',
|
||||||
header: 'Primary Contact',
|
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',
|
id: 'source',
|
||||||
accessorKey: 'source',
|
accessorKey: 'source',
|
||||||
@@ -149,10 +153,7 @@ export function getClientColumns({
|
|||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
||||||
className="text-destructive"
|
|
||||||
onClick={() => onArchive(row.original)}
|
|
||||||
>
|
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</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;
|
id: string;
|
||||||
portId: string;
|
portId: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
companyName: string | null;
|
|
||||||
nationality: 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;
|
preferredContactMethod: string | null;
|
||||||
preferredLanguage: string | null;
|
preferredLanguage: string | null;
|
||||||
timezone: string | null;
|
timezone: string | null;
|
||||||
@@ -46,6 +34,35 @@ interface ClientData {
|
|||||||
name: string;
|
name: string;
|
||||||
color: 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 {
|
interface ClientDetailProps {
|
||||||
@@ -64,11 +81,15 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) {
|
|||||||
'client:updated': [['clients', clientId]],
|
'client:updated': [['clients', clientId]],
|
||||||
'client:archived': [['clients', clientId]],
|
'client:archived': [['clients', clientId]],
|
||||||
'client:restored': [['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
|
const tabs = data ? getClientTabs({ clientId, currentUserId, client: data }) : [];
|
||||||
? getClientTabs({ clientId, currentUserId, client: data })
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailLayout
|
<DetailLayout
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ export const clientFilterDefinitions: FilterDefinition[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Filter by nationality...',
|
placeholder: 'Filter by nationality...',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'isProxy',
|
|
||||||
label: 'Proxy Client',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'includeArchived',
|
key: 'includeArchived',
|
||||||
label: 'Include Archived',
|
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 type { DetailTab } from '@/components/shared/detail-layout';
|
||||||
import { NotesList } from '@/components/shared/notes-list';
|
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 {
|
interface ClientTabsOptions {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
client: {
|
client: {
|
||||||
fullName: string;
|
fullName: string;
|
||||||
companyName?: string | null;
|
|
||||||
nationality?: 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;
|
preferredContactMethod?: string | null;
|
||||||
preferredLanguage?: string | null;
|
preferredLanguage?: string | null;
|
||||||
timezone?: string | null;
|
timezone?: string | null;
|
||||||
@@ -30,6 +24,36 @@ interface ClientTabsOptions {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
isPrimary: boolean;
|
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>
|
<h3 className="text-sm font-medium mb-2">Personal Information</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<InfoRow label="Full Name" value={client.fullName} />
|
<InfoRow label="Full Name" value={client.fullName} />
|
||||||
<InfoRow label="Company" value={client.companyName} />
|
|
||||||
<InfoRow label="Nationality" value={client.nationality} />
|
<InfoRow label="Nationality" value={client.nationality} />
|
||||||
<InfoRow label="Preferred Language" value={client.preferredLanguage} />
|
<InfoRow label="Preferred Language" value={client.preferredLanguage} />
|
||||||
<InfoRow label="Timezone" value={client.timezone} />
|
<InfoRow label="Timezone" value={client.timezone} />
|
||||||
<InfoRow
|
<InfoRow label="Preferred Contact" value={client.preferredContactMethod} />
|
||||||
label="Preferred Contact"
|
|
||||||
value={client.preferredContactMethod}
|
|
||||||
/>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,18 +92,12 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
|||||||
key={c.id}
|
key={c.id}
|
||||||
className="flex items-center gap-2 p-2 rounded-lg border bg-muted/30 text-sm"
|
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">
|
<span className="capitalize text-muted-foreground w-20 shrink-0">{c.channel}</span>
|
||||||
{c.channel}
|
|
||||||
</span>
|
|
||||||
<span className="flex-1">{c.value}</span>
|
<span className="flex-1">{c.value}</span>
|
||||||
{c.label && (
|
{c.label && (
|
||||||
<span className="text-xs text-muted-foreground capitalize">
|
<span className="text-xs text-muted-foreground capitalize">{c.label}</span>
|
||||||
{c.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{c.isPrimary && (
|
|
||||||
<span className="text-xs font-medium text-primary">Primary</span>
|
|
||||||
)}
|
)}
|
||||||
|
{c.isPrimary && <span className="text-xs font-medium text-primary">Primary</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -92,41 +106,6 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Source */}
|
||||||
{(client.source || client.sourceDetails) && (
|
{(client.source || client.sourceDetails) && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -138,34 +117,54 @@ function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Proxy Info */}
|
{/* Tags */}
|
||||||
{client.isProxy && (
|
{client.tags && client.tags.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="text-sm font-medium mb-2">Proxy Information</h3>
|
<h3 className="text-sm font-medium mb-2">Tags</h3>
|
||||||
<dl>
|
<div className="flex flex-wrap gap-1">
|
||||||
<InfoRow
|
{client.tags.map((tag) => (
|
||||||
label="Proxy Type"
|
<span
|
||||||
value={client.proxyType?.replace('_', ' ')}
|
key={tag.id}
|
||||||
/>
|
className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
<InfoRow label="Actual Owner" value={client.actualOwnerName} />
|
style={{ backgroundColor: `${tag.color}20`, color: tag.color }}
|
||||||
</dl>
|
>
|
||||||
|
{tag.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClientTabs({
|
export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOptions): DetailTab[] {
|
||||||
clientId,
|
|
||||||
currentUserId,
|
|
||||||
client,
|
|
||||||
}: ClientTabsOptions): DetailTab[] {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'overview',
|
id: 'overview',
|
||||||
label: 'Overview',
|
label: 'Overview',
|
||||||
content: <OverviewTab client={client} />,
|
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',
|
id: 'interests',
|
||||||
label: 'Interests',
|
label: 'Interests',
|
||||||
@@ -178,13 +177,7 @@ export function getClientTabs({
|
|||||||
{
|
{
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
label: 'Notes',
|
label: 'Notes',
|
||||||
content: (
|
content: <NotesList entityType="clients" entityId={clientId} currentUserId={currentUserId} />,
|
||||||
<NotesList
|
|
||||||
entityType="clients"
|
|
||||||
entityId={clientId}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
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