From f64a52b9955b15b73311581925f2259eef69d586 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 13:44:15 +0200 Subject: [PATCH] feat(ui): yacht list page with columns and filters --- .../(dashboard)/[portSlug]/yachts/page.tsx | 5 + src/components/yachts/yacht-columns.tsx | 176 ++++++++++++++++++ src/components/yachts/yacht-filters.tsx | 34 ++++ src/components/yachts/yacht-list.tsx | 170 +++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 src/app/(dashboard)/[portSlug]/yachts/page.tsx create mode 100644 src/components/yachts/yacht-columns.tsx create mode 100644 src/components/yachts/yacht-filters.tsx create mode 100644 src/components/yachts/yacht-list.tsx diff --git a/src/app/(dashboard)/[portSlug]/yachts/page.tsx b/src/app/(dashboard)/[portSlug]/yachts/page.tsx new file mode 100644 index 0000000..84669ac --- /dev/null +++ b/src/app/(dashboard)/[portSlug]/yachts/page.tsx @@ -0,0 +1,5 @@ +import { YachtList } from '@/components/yachts/yacht-list'; + +export default function YachtsPage() { + return ; +} diff --git a/src/components/yachts/yacht-columns.tsx b/src/components/yachts/yacht-columns.tsx new file mode 100644 index 0000000..0df67c5 --- /dev/null +++ b/src/components/yachts/yacht-columns.tsx @@ -0,0 +1,176 @@ +'use client'; + +import Link from 'next/link'; +import { MoreHorizontal, Pencil, Archive, Eye } from 'lucide-react'; +import type { ColumnDef } from '@tanstack/react-table'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { OwnerLink } from '@/components/yachts/yacht-detail-header'; + +export interface YachtRow { + id: string; + name: string; + hullNumber: string | null; + registration: string | null; + currentOwnerType: 'client' | 'company'; + currentOwnerId: string; + lengthFt: string | null; + widthFt: string | null; + draftFt: string | null; + lengthM: string | null; + widthM: string | null; + status: string; + archivedAt: string | null; + updatedAt: string; +} + +const STATUS_COLORS: Record = { + active: 'bg-green-100 text-green-800 border-green-300', + retired: 'bg-gray-100 text-gray-800 border-gray-300', + sold_away: 'bg-amber-100 text-amber-800 border-amber-300', +}; + +const STATUS_LABELS: Record = { + active: 'Active', + retired: 'Retired', + sold_away: 'Sold Away', +}; + +function formatDimensions(yacht: YachtRow): string | null { + if (yacht.lengthFt || yacht.widthFt) { + const length = yacht.lengthFt ?? '—'; + const width = yacht.widthFt ?? '—'; + return `${length} × ${width} ft`; + } + if (yacht.lengthM || yacht.widthM) { + const length = yacht.lengthM ?? '—'; + const width = yacht.widthM ?? '—'; + return `${length} × ${width} m`; + } + return null; +} + +interface GetYachtColumnsOptions { + portSlug: string; + onEdit: (yacht: YachtRow) => void; + onArchive: (yacht: YachtRow) => void; +} + +export function getYachtColumns({ + portSlug, + onEdit, + onArchive, +}: GetYachtColumnsOptions): ColumnDef[] { + return [ + { + id: 'name', + accessorKey: 'name', + header: 'Name', + cell: ({ row }) => ( + e.stopPropagation()} + > + {row.original.name} + + ), + }, + { + id: 'currentOwner', + header: 'Current Owner', + enableSorting: false, + cell: ({ row }) => ( + + ), + }, + { + id: 'dimensions', + header: 'Dimensions', + enableSorting: false, + cell: ({ row }) => { + const dims = formatDimensions(row.original); + if (!dims) return ; + return {dims}; + }, + }, + { + id: 'hullNumber', + accessorKey: 'hullNumber', + header: 'Hull Number', + enableSorting: false, + cell: ({ getValue }) => { + const value = getValue() as string | null; + if (!value) return ; + return {value}; + }, + }, + { + id: 'status', + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => { + const status = row.original.status; + const label = STATUS_LABELS[status] ?? status; + const color = STATUS_COLORS[status] ?? 'bg-muted text-muted-foreground border-muted'; + return ( + + {label} + + ); + }, + }, + { + id: 'actions', + header: '', + enableSorting: false, + size: 48, + cell: ({ row }) => ( + + + + + + + + + View + + + onEdit(row.original)}> + + Edit + + onArchive(row.original)}> + + Archive + + + + ), + }, + ]; +} diff --git a/src/components/yachts/yacht-filters.tsx b/src/components/yachts/yacht-filters.tsx new file mode 100644 index 0000000..c5ef74d --- /dev/null +++ b/src/components/yachts/yacht-filters.tsx @@ -0,0 +1,34 @@ +import type { FilterDefinition } from '@/components/shared/filter-bar'; + +export const yachtFilterDefinitions: FilterDefinition[] = [ + { + key: 'search', + label: 'Search', + type: 'text', + placeholder: 'Search by name, hull, registration...', + }, + { + key: 'status', + label: 'Status', + type: 'select', + options: [ + { label: 'Active', value: 'active' }, + { label: 'Retired', value: 'retired' }, + { label: 'Sold Away', value: 'sold_away' }, + ], + }, + { + key: 'ownerType', + label: 'Owner Type', + type: 'select', + options: [ + { label: 'Client', value: 'client' }, + { label: 'Company', value: 'company' }, + ], + }, + { + key: 'includeArchived', + label: 'Include Archived', + type: 'boolean', + }, +]; diff --git a/src/components/yachts/yacht-list.tsx b/src/components/yachts/yacht-list.tsx new file mode 100644 index 0000000..90d0994 --- /dev/null +++ b/src/components/yachts/yacht-list.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { useState } from 'react'; +import { useParams } from 'next/navigation'; +import { Plus } from 'lucide-react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { Button } from '@/components/ui/button'; +import { DataTable } from '@/components/shared/data-table'; +import { FilterBar } from '@/components/shared/filter-bar'; +import { SavedViewsDropdown } from '@/components/shared/saved-views-dropdown'; +import { PageHeader } from '@/components/shared/page-header'; +import { EmptyState } from '@/components/shared/empty-state'; +import { TableSkeleton } from '@/components/shared/loading-skeleton'; +import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; +import { PermissionGate } from '@/components/shared/permission-gate'; +import { YachtForm } from '@/components/yachts/yacht-form'; +import { yachtFilterDefinitions } from '@/components/yachts/yacht-filters'; +import { getYachtColumns, type YachtRow } from '@/components/yachts/yacht-columns'; +import { usePaginatedQuery } from '@/hooks/use-paginated-query'; +import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; +import { apiFetch } from '@/lib/api/client'; + +export function YachtList() { + const params = useParams<{ portSlug: string }>(); + const portSlug = params?.portSlug ?? ''; + const queryClient = useQueryClient(); + + const [createOpen, setCreateOpen] = useState(false); + const [editYacht, setEditYacht] = useState(null); + const [archiveYacht, setArchiveYacht] = useState(null); + + const { + data, + pagination, + isLoading, + isFetching, + sort, + setSort, + setPage, + setPageSize, + filters, + setFilter, + clearFilters, + } = usePaginatedQuery({ + queryKey: ['yachts'], + endpoint: '/api/v1/yachts', + filterDefinitions: yachtFilterDefinitions, + }); + + useRealtimeInvalidation({ + 'yacht:created': [['yachts']], + 'yacht:updated': [['yachts']], + 'yacht:archived': [['yachts']], + 'yacht:ownership_transferred': [['yachts']], + }); + + const archiveMutation = useMutation({ + mutationFn: (id: string) => apiFetch(`/api/v1/yachts/${id}`, { method: 'DELETE' }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['yachts'] }); + setArchiveYacht(null); + }, + }); + + const columns = getYachtColumns({ + portSlug, + onEdit: (yacht) => setEditYacht(yacht), + onArchive: (yacht) => setArchiveYacht(yacht), + }); + + return ( +
+ + + + } + /> + +
+ + { + clearFilters(); + Object.entries(savedFilters).forEach(([key, val]) => setFilter(key, val)); + }} + /> +
+ + {isLoading ? ( + + ) : !data.length ? ( + setCreateOpen(true) }} + /> + ) : ( + { + setPage(p); + setPageSize(ps); + }} + sort={sort} + onSortChange={setSort} + isLoading={isFetching && !isLoading} + getRowId={(row) => row.id} + emptyState={ + setCreateOpen(true) }} + /> + } + /> + )} + + + + {editYacht && ( + !open && setEditYacht(null)} + yacht={{ + id: editYacht.id, + name: editYacht.name, + hullNumber: editYacht.hullNumber, + registration: editYacht.registration, + lengthFt: editYacht.lengthFt, + widthFt: editYacht.widthFt, + draftFt: editYacht.draftFt, + lengthM: editYacht.lengthM, + widthM: editYacht.widthM, + currentOwnerType: editYacht.currentOwnerType, + currentOwnerId: editYacht.currentOwnerId, + status: editYacht.status, + }} + /> + )} + + !open && setArchiveYacht(null)} + entityName={archiveYacht?.name ?? ''} + entityType="Yacht" + isArchived={false} + onConfirm={() => archiveYacht && archiveMutation.mutate(archiveYacht.id)} + isLoading={archiveMutation.isPending} + /> +
+ ); +}