feat(ui): yacht detail page with header, tabs, ownership history
Implements Task 5.3: server page passes yachtId to a client YachtDetail, which fetches via TanStack Query and renders the shared DetailLayout with Overview / Ownership History / Interests / Reservations / Notes / Tags tabs. Header shows name, dimensions, polymorphic owner link, status badge, and Edit / Transfer / Archive actions. Transfer is a stub dialog pending Task 5.5; Notes tab is a placeholder because NotesList does not yet support entityType='yachts'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
168
src/components/yachts/yacht-tabs.tsx
Normal file
168
src/components/yachts/yacht-tabs.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import type { DetailTab } from '@/components/shared/detail-layout';
|
||||
import { EmptyState } from '@/components/shared/empty-state';
|
||||
import { YachtOwnershipHistory } from '@/components/yachts/yacht-ownership-history';
|
||||
|
||||
interface YachtTabsYacht {
|
||||
id: 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;
|
||||
status: string;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
interface YachtTabsOptions {
|
||||
yachtId: string;
|
||||
currentUserId?: string;
|
||||
yacht: YachtTabsYacht;
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value?: string | number | null }) {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
return (
|
||||
<div className="flex gap-2 py-1.5 border-b last:border-0">
|
||||
<dt className="w-40 shrink-0 text-sm text-muted-foreground">{label}</dt>
|
||||
<dd className="text-sm">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
active: 'Active',
|
||||
retired: 'Retired',
|
||||
sold_away: 'Sold away',
|
||||
};
|
||||
|
||||
function OverviewTab({ yacht }: { yacht: YachtTabsYacht }) {
|
||||
const hasFtDimensions = yacht.lengthFt || yacht.widthFt || yacht.draftFt;
|
||||
const hasMDimensions = yacht.lengthM || yacht.widthM || yacht.draftM;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Identity */}
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Identity</h3>
|
||||
<dl>
|
||||
<InfoRow label="Name" value={yacht.name} />
|
||||
<InfoRow label="Hull Number" value={yacht.hullNumber} />
|
||||
<InfoRow label="Registration" value={yacht.registration} />
|
||||
<InfoRow label="Flag" value={yacht.flag} />
|
||||
<InfoRow label="Year Built" value={yacht.yearBuilt} />
|
||||
<InfoRow label="Status" value={STATUS_LABELS[yacht.status] ?? yacht.status} />
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* Build */}
|
||||
{(yacht.builder || yacht.model || yacht.hullMaterial) && (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Build</h3>
|
||||
<dl>
|
||||
<InfoRow label="Builder" value={yacht.builder} />
|
||||
<InfoRow label="Model" value={yacht.model} />
|
||||
<InfoRow label="Hull Material" value={yacht.hullMaterial} />
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dimensions (ft) */}
|
||||
{hasFtDimensions && (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Dimensions (ft)</h3>
|
||||
<dl>
|
||||
<InfoRow label="Length" value={yacht.lengthFt ? `${yacht.lengthFt} ft` : null} />
|
||||
<InfoRow label="Width" value={yacht.widthFt ? `${yacht.widthFt} ft` : null} />
|
||||
<InfoRow label="Draft" value={yacht.draftFt ? `${yacht.draftFt} ft` : null} />
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dimensions (m) */}
|
||||
{hasMDimensions && (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Dimensions (m)</h3>
|
||||
<dl>
|
||||
<InfoRow label="Length" value={yacht.lengthM ? `${yacht.lengthM} m` : null} />
|
||||
<InfoRow label="Width" value={yacht.widthM ? `${yacht.widthM} m` : null} />
|
||||
<InfoRow label="Draft" value={yacht.draftM ? `${yacht.draftM} m` : null} />
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
{yacht.notes && (
|
||||
<div className="space-y-1 md:col-span-2">
|
||||
<h3 className="text-sm font-medium mb-2">Notes</h3>
|
||||
<p className="text-sm whitespace-pre-wrap rounded-md border bg-muted/30 p-3">
|
||||
{yacht.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function getYachtTabs({
|
||||
yachtId,
|
||||
// currentUserId reserved for when NotesList supports entityType='yachts'.
|
||||
currentUserId: _currentUserId,
|
||||
yacht,
|
||||
}: YachtTabsOptions): DetailTab[] {
|
||||
void _currentUserId;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
content: <OverviewTab yacht={yacht} />,
|
||||
},
|
||||
{
|
||||
id: 'ownership-history',
|
||||
label: 'Ownership History',
|
||||
content: <YachtOwnershipHistory yachtId={yachtId} />,
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
label: 'Interests',
|
||||
content: <EmptyState title="Interests" description="Coming soon" />,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Reservations',
|
||||
content: <EmptyState title="Reservations" description="Coming soon" />,
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
label: 'Notes',
|
||||
// TODO: NotesList currently supports entityType 'clients' | 'interests'.
|
||||
// Extend NotesList (or swap to a yacht-notes endpoint) in a follow-up.
|
||||
content: (
|
||||
<EmptyState
|
||||
title="Notes"
|
||||
description="Yacht notes coming soon — the notes endpoint is pending wiring."
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
label: 'Tags',
|
||||
// TODO: replace with an inline tag editor once one exists; yacht tags
|
||||
// can be edited via the Edit form in the meantime.
|
||||
content: (
|
||||
<EmptyState title="Tags" description="Manage tags from the Edit yacht form for now." />
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user