From 2111bb8b601480dc2f82d88cf586e11400cbf546 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 14:18:11 +0200 Subject: [PATCH] feat(ui): add reservation-list table component --- .../reservations/reservation-list.tsx | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/components/reservations/reservation-list.tsx diff --git a/src/components/reservations/reservation-list.tsx b/src/components/reservations/reservation-list.tsx new file mode 100644 index 0000000..413dd7f --- /dev/null +++ b/src/components/reservations/reservation-list.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { EmptyState } from '@/components/shared/empty-state'; +import { apiFetch } from '@/lib/api/client'; + +export interface ReservationRow { + id: string; + berthId: string; + portId: string; + clientId: string; + yachtId: string; + status: 'pending' | 'active' | 'ended' | 'cancelled'; + startDate: string; + endDate: string | null; + tenureType: string; + contractFileId: string | null; + notes: string | null; + createdAt: string; +} + +export interface ReservationListProps { + reservations: ReservationRow[]; + showBerth?: boolean; + portSlug?: string; + emptyMessage?: string; +} + +/** + * Renders a client's name as a link by fetching the client record. + * Uses TanStack Query cache for memoization of repeated clientId queries. + */ +function ClientLink({ clientId, portSlug }: { clientId: string; portSlug: string }) { + const { data } = useQuery<{ fullName: string }>({ + queryKey: ['clients', clientId, 'name-only'], + queryFn: () => + apiFetch<{ data: { fullName: string } }>(`/api/v1/clients/${clientId}`).then((r) => r.data), + }); + + return ( + + {data?.fullName ?? `Client ${clientId.slice(0, 8)}`} + + ); +} + +/** + * Renders a yacht's name as a link by fetching the yacht record. + */ +function YachtLink({ yachtId, portSlug }: { yachtId: string; portSlug: string }) { + const { data } = useQuery<{ name: string }>({ + queryKey: ['yachts', yachtId, 'name-only'], + queryFn: () => + apiFetch<{ data: { name: string } }>(`/api/v1/yachts/${yachtId}`).then((r) => r.data), + }); + + return ( + + {data?.name ?? `Yacht ${yachtId.slice(0, 8)}`} + + ); +} + +/** + * Renders a berth's mooring number as a link by fetching the berth record. + */ +function BerthLink({ berthId, portSlug }: { berthId: string; portSlug: string }) { + const { data } = useQuery<{ mooringNumber: string }>({ + queryKey: ['berths', berthId, 'name-only'], + queryFn: () => + apiFetch<{ data: { mooringNumber: string } }>(`/api/v1/berths/${berthId}`).then( + (r) => r.data, + ), + }); + + return ( + + {data?.mooringNumber ?? `Berth ${berthId.slice(0, 8)}`} + + ); +} + +/** + * Renders a status badge with appropriate color coding. + */ +function StatusBadge({ status }: { status: ReservationRow['status'] }) { + const colorMap: Record = { + pending: 'bg-gray-100 text-gray-800', + active: 'bg-green-100 text-green-800', + ended: 'bg-blue-100 text-blue-800', + cancelled: 'bg-red-100 text-red-800', + }; + + const color = colorMap[status]; + const label = status.charAt(0).toUpperCase() + status.slice(1); + + return ( + + {label} + + ); +} + +/** + * Pretty-prints tenure type for display. + */ +function prettyTenure(tenureType: string): string { + const tenureMap: Record = { + permanent: 'Permanent', + fixed_term: 'Fixed term', + seasonal: 'Seasonal', + }; + return tenureMap[tenureType] ?? tenureType; +} + +/** + * Formats a date range as "{startDate} → {endDate or 'ongoing'}". + */ +function formatDateRange(startDate: string, endDate: string | null): string { + const start = new Date(startDate).toLocaleDateString(); + const end = endDate ? new Date(endDate).toLocaleDateString() : 'ongoing'; + return `${start} → ${end}`; +} + +export function ReservationList({ + reservations, + showBerth = false, + portSlug: portSlugProp, + emptyMessage, +}: ReservationListProps) { + const routeParams = useParams<{ portSlug: string }>(); + const portSlug = portSlugProp ?? routeParams?.portSlug ?? ''; + + if (reservations.length === 0) { + return ( + + ); + } + + return ( +
+ + + + {showBerth && Berth} + Client + Yacht + Dates + Tenure + Status + Contract + + + + {reservations.map((r) => ( + + {showBerth && ( + + + + )} + + + + + + + {formatDateRange(r.startDate, r.endDate)} + {prettyTenure(r.tenureType)} + + + + + {r.contractFileId ? ( + // TODO: Confirm final file-download endpoint URL when available + + View contract + + ) : ( + '—' + )} + + + ))} + +
+
+ ); +}