'use client'; import Link from 'next/link'; import { useRouter, useParams } from 'next/navigation'; import { Anchor, Plus } from 'lucide-react'; import { DataTable } from '@/components/shared/data-table'; import { FilterBar } from '@/components/shared/filter-bar'; import { PageHeader } from '@/components/shared/page-header'; import { SavedViewsDropdown } from '@/components/shared/saved-views-dropdown'; import { ColumnPicker } from '@/components/shared/column-picker'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { EmptyState } from '@/components/shared/empty-state'; import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { usePermissions } from '@/hooks/use-permissions'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useTablePreferences } from '@/hooks/use-table-preferences'; import { BerthCard } from './berth-card'; import { berthColumns, BERTH_COLUMN_OPTIONS, BERTH_DEFAULT_HIDDEN, type BerthRow, } from './berth-columns'; import { berthFilterDefinitions } from './berth-filters'; import { mooringLetterTone } from './mooring-letter-tone'; export function BerthList() { const router = useRouter(); const params = useParams<{ portSlug: string }>(); // F13: bulk-add wizard had no UI entry point. Gate the CTA on // `berths.import` (the existing permission used for adding berths) // so non-admins don't see a button that 403s on click. const { can } = usePermissions(); const canBulkAdd = can('berths', 'import'); const { data, pagination, isLoading, sort, setSort, filters, setFilter, setAllFilters, clearFilters, setPage, setPageSize, } = usePaginatedQuery({ queryKey: ['berths'], endpoint: '/api/v1/berths', filterDefinitions: berthFilterDefinitions, }); useRealtimeInvalidation({ 'berth:updated': [['berths']], 'berth:statusChanged': [['berths']], }); // Persisted column visibility — same pattern as ClientList / InterestList. const { hidden, setHidden } = useTablePreferences('berths', BERTH_DEFAULT_HIDDEN); const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false])); return (
{/* Toolbar — two halves separated by `justify-between` so the Columns + Saved-views actions stay pinned to the right edge of the row at every width. The previous `ml-auto` trick didn't survive flex-wrap on intermediate widths — the actions ended up centered. */}
d.key !== 'search')} values={filters} onChange={setFilter} onClear={clearFilters} /> setFilter('search', e.target.value || undefined)} className="h-8 min-w-0 flex-1 sm:max-w-xs" />
{ setAllFilters(savedFilters); }} />
columns={berthColumns} columnVisibility={columnVisibility} data={data} isLoading={isLoading} pagination={{ page: pagination.page, pageSize: pagination.pageSize, total: pagination.total, totalPages: pagination.totalPages, }} onPaginationChange={(page, pageSize) => { setPage(page); setPageSize(pageSize); }} sort={sort} onSortChange={setSort} getRowId={(row) => row.id} onRowClick={(row) => router.push(`/${params.portSlug}/berths/${row.id}`)} getRowClassName={(row) => mooringLetterTone(row.mooringNumber)} cardRender={(row) => } // Group adjacent cards by dock letter (area) on mobile — adds a // dim divider + uppercased label above the first card of each // group. Data is already sorted by mooringNumber (A1, A2, …, B1, // B2, …) so consecutive rows naturally share dock letters. mobileGroupBy={(row) => row.area ?? 'Unassigned'} emptyState={ // Distinguish "no data at all" (fresh port, run the importer) // from "no rows after filtering" (adjust filters). The original // copy implied data existed but was hidden, which misled fresh- // port admins for whom there is literally nothing yet. Object.values(filters).every((v) => v === undefined || v === '') ? ( ) : ( ) } />
); }