'use client'; import { type ColumnDef } from '@tanstack/react-table'; import { MoreHorizontal, Pencil, Activity } from 'lucide-react'; import { useRouter, useParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { TagBadge } from '@/components/shared/tag-badge'; import { formatCurrency } from '@/lib/utils/currency'; import { mooringLetterDot } from './mooring-letter-tone'; export type BerthRow = { id: string; mooringNumber: string; area: string | null; status: string; // Dimensions (both units; row falls back when one is null) lengthFt: string | null; widthFt: string | null; draftFt: string | null; lengthM: string | null; widthM: string | null; draftM: string | null; widthIsMinimum: boolean | null; // Capacity nominalBoatSize: string | null; nominalBoatSizeM: string | null; waterDepth: string | null; waterDepthM: string | null; waterDepthIsMinimum: boolean | null; // Pontoon details (NocoDB) sidePontoon: string | null; mooringType: string | null; cleatType: string | null; cleatCapacity: string | null; bollardType: string | null; bollardCapacity: string | null; access: string | null; bowFacing: string | null; berthApproved: boolean | null; // Power powerCapacity: string | null; voltage: string | null; // Pricing price: string | null; priceCurrency: string; weeklyRateHighUsd: string | null; weeklyRateLowUsd: string | null; dailyRateHighUsd: string | null; dailyRateLowUsd: string | null; pricingValidUntil: string | null; // Tenure tenureType: string; tenureYears: number | null; tenureStartDate: string | null; tenureEndDate: string | null; tags: Array<{ id: string; name: string; color: string }>; }; /** * Toggleable columns for the berth list ColumnPicker. Heavy NocoDB * fields default to hidden; reps can switch them on per-table-view. * `mooringNumber` is intentionally omitted from this list — it's the * primary identifier and always visible. */ export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [ { id: 'area', label: 'Area' }, { id: 'status', label: 'Status' }, { id: 'sidePontoon', label: 'Side / Pontoon' }, { id: 'dimensions', label: 'Dimensions' }, { id: 'nominalBoatSize', label: 'Nominal boat size' }, { id: 'waterDepth', label: 'Water depth' }, { id: 'mooringType', label: 'Mooring type' }, { id: 'cleat', label: 'Cleat (type · capacity)' }, { id: 'bollard', label: 'Bollard (type · capacity)' }, { id: 'access', label: 'Access' }, { id: 'bowFacing', label: 'Bow facing' }, { id: 'berthApproved', label: 'Approved' }, { id: 'power', label: 'Power (kW · V)' }, { id: 'price', label: 'Price' }, { id: 'rates', label: 'Daily / Weekly rates' }, { id: 'pricingValidUntil', label: 'Pricing valid until' }, { id: 'tenure', label: 'Tenure' }, { id: 'tags', label: 'Tags' }, ]; /** Hidden by default — power-users turn them on via the picker. */ export const BERTH_DEFAULT_HIDDEN: string[] = [ 'tenure', 'sidePontoon', 'nominalBoatSize', 'waterDepth', 'mooringType', 'cleat', 'bollard', 'access', 'bowFacing', 'berthApproved', 'power', 'rates', 'pricingValidUntil', ]; function StatusBadge({ status }: { status: string }) { const variants: Record = { available: 'bg-green-100 text-green-800 border-green-200', under_offer: 'bg-yellow-100 text-yellow-800 border-yellow-200', sold: 'bg-red-100 text-red-800 border-red-200', }; const labels: Record = { available: 'Available', under_offer: 'Under Offer', sold: 'Sold', }; return ( {labels[status] ?? status} ); } function ActionsCell({ row }: { row: { original: BerthRow } }) { const router = useRouter(); const params = useParams<{ portSlug: string }>(); const berth = row.original; return ( { e.stopPropagation(); router.push(`/${params.portSlug}/berths/${berth.id}`); }} > View details { e.stopPropagation(); router.push(`/${params.portSlug}/berths/${berth.id}?edit=true`); }} > Edit ); } function joinNonNull(parts: Array, sep = ' · '): string { return parts.filter((p): p is string => Boolean(p)).join(sep); } function formatMoney(amount: string | null, currency: string): string | null { if (!amount) return null; return formatCurrency(amount, currency, { maxFractionDigits: 0 }); } export const berthColumns: ColumnDef[] = [ { accessorKey: 'mooringNumber', header: 'Mooring #', cell: ({ row }) => { const dot = mooringLetterDot(row.original.mooringNumber); return ( {dot && } {row.original.mooringNumber} ); }, }, { id: 'area', accessorKey: 'area', header: 'Area', cell: ({ row }) => row.original.area ?? '-', }, { id: 'status', accessorKey: 'status', header: 'Status', cell: ({ row }) => , }, { id: 'sidePontoon', header: 'Side / Pontoon', enableSorting: false, cell: ({ row }) => row.original.sidePontoon ?? '-', }, { id: 'dimensions', header: 'Dimensions', enableSorting: false, cell: ({ row }) => { const { lengthM, widthM, draftM, widthIsMinimum } = row.original; if (!lengthM && !widthM) return '-'; const widthLabel = widthM ? `${widthIsMinimum ? '≥' : ''}${widthM}m` : '?'; const base = `${lengthM ?? '?'}m × ${widthLabel}`; return draftM ? `${base} (draft ${draftM}m)` : base; }, }, { id: 'nominalBoatSize', header: 'Boat size', enableSorting: false, cell: ({ row }) => { const m = row.original.nominalBoatSizeM; const ft = row.original.nominalBoatSize; if (!m && !ft) return '-'; return m ? `${m}m` : `${ft}ft`; }, }, { id: 'waterDepth', header: 'Water depth', enableSorting: false, cell: ({ row }) => { const { waterDepthM, waterDepthIsMinimum } = row.original; if (!waterDepthM) return '-'; return `${waterDepthIsMinimum ? '≥' : ''}${waterDepthM}m`; }, }, { id: 'mooringType', header: 'Mooring type', enableSorting: false, cell: ({ row }) => row.original.mooringType ?? '-', }, { id: 'cleat', header: 'Cleat', enableSorting: false, cell: ({ row }) => joinNonNull([row.original.cleatType, row.original.cleatCapacity]) || '-', }, { id: 'bollard', header: 'Bollard', enableSorting: false, cell: ({ row }) => joinNonNull([row.original.bollardType, row.original.bollardCapacity]) || '-', }, { id: 'access', header: 'Access', enableSorting: false, cell: ({ row }) => row.original.access ?? '-', }, { id: 'bowFacing', header: 'Bow facing', enableSorting: false, cell: ({ row }) => row.original.bowFacing ?? '-', }, { id: 'berthApproved', header: 'Approved', enableSorting: false, cell: ({ row }) => (row.original.berthApproved ? 'Yes' : 'No'), }, { id: 'power', header: 'Power', enableSorting: false, cell: ({ row }) => { const kw = row.original.powerCapacity; const v = row.original.voltage; if (!kw && !v) return '-'; return joinNonNull([kw ? `${kw}kW` : null, v ? `${v}V` : null]); }, }, { id: 'price', accessorKey: 'price', header: 'Price', cell: ({ row }) => formatMoney(row.original.price, row.original.priceCurrency) ?? '-', }, { id: 'rates', header: 'Rates (USD)', enableSorting: false, cell: ({ row }) => { const { dailyRateLowUsd, dailyRateHighUsd, weeklyRateLowUsd, weeklyRateHighUsd } = row.original; const daily = dailyRateLowUsd && dailyRateHighUsd ? `${dailyRateLowUsd}–${dailyRateHighUsd}/d` : dailyRateLowUsd ? `${dailyRateLowUsd}/d` : null; const weekly = weeklyRateLowUsd && weeklyRateHighUsd ? `${weeklyRateLowUsd}–${weeklyRateHighUsd}/wk` : weeklyRateLowUsd ? `${weeklyRateLowUsd}/wk` : null; return joinNonNull([daily, weekly]) || '-'; }, }, { id: 'pricingValidUntil', header: 'Pricing valid', enableSorting: false, cell: ({ row }) => row.original.pricingValidUntil ?? '-', }, { id: 'tenure', accessorKey: 'tenureType', header: 'Tenure', cell: ({ row }) => (row.original.tenureType === 'permanent' ? 'Permanent' : 'Fixed Term'), }, { id: 'tags', header: 'Tags', enableSorting: false, cell: ({ row }) => { const { tags } = row.original; if (!tags || tags.length === 0) return null; return (
{tags.slice(0, 3).map((tag) => ( ))} {tags.length > 3 && ( +{tags.length - 3} )}
); }, }, { id: 'actions', header: '', enableSorting: false, size: 48, cell: ({ row }) => , }, ];