'use client'; import Link from 'next/link'; import { format, formatDistanceToNowStrict } from 'date-fns'; import { MoreHorizontal, Pencil, Archive, MessageSquare } 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 { Badge } from '@/components/ui/badge'; import { stageBadgeClass, stageLabel } from '@/lib/constants'; import { computeUrgencyBadges, type InterestUrgencyInput } from '@/components/interests/urgency'; export interface InterestRow { id: string; clientId: string; clientName: string | null; yachtId?: string | null; yachtName?: string | null; berthId: string | null; berthMooringNumber: string | null; pipelineStage: string; leadCategory: string | null; source: string | null; archivedAt: string | null; createdAt: string; /** Surfaced by listInterests for the row-level sales-triage signals * (last-activity relative time, comment-icon, urgency badges). */ updatedAt?: string; dateLastContact?: string | null; dateEoiSent?: string | null; dateDepositReceived?: string | null; eoiStatus?: string | null; outcome?: string | null; /** Imperial; nullable. Recommender treats nulls as "no constraint" on * that axis. Rendered as a compact "60×18×6 ft" string in the list. */ desiredLengthFt?: string | number | null; desiredWidthFt?: string | number | null; desiredDraftFt?: string | number | null; notesCount?: number; tags?: Array<{ id: string; name: string; color: string }>; } function formatDim(value: string | number | null | undefined): string { if (value === null || value === undefined || value === '') return '?'; const n = typeof value === 'number' ? value : parseFloat(value); if (!Number.isFinite(n)) return '?'; return Number.isInteger(n) ? String(n) : n.toFixed(1); } function formatDesiredSize(row: InterestRow): string | null { const { desiredLengthFt, desiredWidthFt, desiredDraftFt } = row; if ( (desiredLengthFt === null || desiredLengthFt === undefined || desiredLengthFt === '') && (desiredWidthFt === null || desiredWidthFt === undefined || desiredWidthFt === '') && (desiredDraftFt === null || desiredDraftFt === undefined || desiredDraftFt === '') ) { return null; } return `${formatDim(desiredLengthFt)}×${formatDim(desiredWidthFt)}×${formatDim(desiredDraftFt)} ft`; } const SOURCE_LABELS: Record = { website: 'Website', manual: 'Manual', referral: 'Referral', broker: 'Broker', }; /** * Toggleable columns for the InterestList ColumnPicker. `actions` and * `clientName` are intentionally omitted from this list — actions is a * row-control column that should never be hidden, and clientName is the * primary entity identifier (a row with no name has no useful purpose). */ export const INTEREST_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [ { id: 'yachtName', label: 'Yacht' }, { id: 'berthMooringNumber', label: 'Berth' }, { id: 'desiredSize', label: 'Desired size' }, { id: 'pipelineStage', label: 'Stage' }, { id: 'eoiStatus', label: 'EOI status' }, { id: 'source', label: 'Source' }, { id: 'dateLastContact', label: 'Last contact' }, ]; /** * Columns hidden by default for users who haven't customised their view. * Keep the busy `desiredSize` and `eoiStatus` collapsed by default — * power-users can turn them back on via the column picker. */ export const INTEREST_DEFAULT_HIDDEN: string[] = ['desiredSize', 'eoiStatus']; const EOI_STATUS_LABELS: Record = { waiting_for_signatures: { label: 'Waiting', tone: 'bg-amber-100 text-amber-900' }, signed: { label: 'Signed', tone: 'bg-emerald-100 text-emerald-900' }, expired: { label: 'Expired', tone: 'bg-rose-100 text-rose-900' }, }; interface GetColumnsOptions { portSlug: string; onEdit: (interest: InterestRow) => void; onArchive: (interest: InterestRow) => void; } export function getInterestColumns({ portSlug, onEdit, onArchive, }: GetColumnsOptions): ColumnDef[] { return [ { id: 'clientName', accessorKey: 'clientName', header: 'Client', cell: ({ row }) => { const notesCount = row.original.notesCount ?? 0; return (
e.stopPropagation()} > {row.original.clientName ?? '-'} {notesCount > 0 ? ( ) : null}
); }, }, { id: 'yachtName', accessorKey: 'yachtName', header: 'Yacht', enableSorting: false, cell: ({ row }) => { const name = row.original.yachtName; if (!name) return -; const yachtId = row.original.yachtId; if (!yachtId) return {name}; return ( e.stopPropagation()} > {name} ); }, }, { id: 'berthMooringNumber', accessorKey: 'berthMooringNumber', header: 'Berth', cell: ({ row }) => { if (!row.original.berthId || !row.original.berthMooringNumber) { return -; } return ( e.stopPropagation()} > {row.original.berthMooringNumber} ); }, }, { id: 'desiredSize', header: 'Berth size desired', enableSorting: false, cell: ({ row }) => { const label = formatDesiredSize(row.original); if (!label) return -; return {label}; }, }, { id: 'pipelineStage', accessorKey: 'pipelineStage', header: 'Stage', cell: ({ row }) => { const stage = row.original.pipelineStage; const badges = computeUrgencyBadges(row.original satisfies InterestUrgencyInput); return ( {stageLabel(stage)} {badges.length > 0 ? (
{badges.map((b) => ( {b.label} ))}
) : null} ); }, }, { id: 'eoiStatus', accessorKey: 'eoiStatus', header: 'EOI status', enableSorting: false, cell: ({ getValue }) => { const status = getValue() as string | null; if (!status) return -; const meta = EOI_STATUS_LABELS[status]; return ( {meta?.label ?? status} ); }, }, { id: 'source', accessorKey: 'source', header: 'Source', cell: ({ getValue }) => { const source = getValue() as string | null; if (!source) return -; return ( {SOURCE_LABELS[source] ?? source} ); }, }, { // Sales-triage default: prefer the explicit dateLastContact, fall back // to updatedAt. Sortable on dateLastContact server-side; the column // header label ("Last activity") makes the fallback semantics clear. id: 'dateLastContact', accessorKey: 'dateLastContact', header: 'Last activity', cell: ({ row }) => { const lastIso = row.original.dateLastContact ?? row.original.updatedAt ?? null; if (!lastIso) { return -; } const d = new Date(lastIso); return ( {formatDistanceToNowStrict(d, { addSuffix: true })} ); }, }, { id: 'actions', header: '', enableSorting: false, size: 48, cell: ({ row }) => ( onEdit(row.original)}> Edit onArchive(row.original)}> Archive ), }, ]; }