221 lines
6.2 KiB
TypeScript
221 lines
6.2 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import Link from 'next/link';
|
||
|
|
import { format } from 'date-fns';
|
||
|
|
import { MoreHorizontal, Pencil, Archive } 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 { TagBadge } from '@/components/shared/tag-badge';
|
||
|
|
|
||
|
|
export interface InterestRow {
|
||
|
|
id: string;
|
||
|
|
clientId: string;
|
||
|
|
clientName: string | null;
|
||
|
|
berthId: string | null;
|
||
|
|
berthMooringNumber: string | null;
|
||
|
|
pipelineStage: string;
|
||
|
|
leadCategory: string | null;
|
||
|
|
source: string | null;
|
||
|
|
archivedAt: string | null;
|
||
|
|
createdAt: string;
|
||
|
|
tags?: Array<{ id: string; name: string; color: string }>;
|
||
|
|
}
|
||
|
|
|
||
|
|
const STAGE_LABELS: Record<string, string> = {
|
||
|
|
open: 'Open',
|
||
|
|
details_sent: 'Details Sent',
|
||
|
|
in_communication: 'In Communication',
|
||
|
|
visited: 'Visited',
|
||
|
|
signed_eoi_nda: 'Signed EOI/NDA',
|
||
|
|
deposit_10pct: 'Deposit 10%',
|
||
|
|
contract: 'Contract',
|
||
|
|
completed: 'Completed',
|
||
|
|
};
|
||
|
|
|
||
|
|
const STAGE_COLORS: Record<string, string> = {
|
||
|
|
open: 'bg-slate-100 text-slate-700',
|
||
|
|
details_sent: 'bg-blue-100 text-blue-700',
|
||
|
|
in_communication: 'bg-sky-100 text-sky-700',
|
||
|
|
visited: 'bg-violet-100 text-violet-700',
|
||
|
|
signed_eoi_nda: 'bg-amber-100 text-amber-700',
|
||
|
|
deposit_10pct: 'bg-orange-100 text-orange-700',
|
||
|
|
contract: 'bg-green-100 text-green-700',
|
||
|
|
completed: 'bg-emerald-100 text-emerald-700',
|
||
|
|
};
|
||
|
|
|
||
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
||
|
|
general_interest: 'General Interest',
|
||
|
|
specific_qualified: 'Specific Qualified',
|
||
|
|
hot_lead: 'Hot Lead',
|
||
|
|
};
|
||
|
|
|
||
|
|
const SOURCE_LABELS: Record<string, string> = {
|
||
|
|
website: 'Website',
|
||
|
|
manual: 'Manual',
|
||
|
|
referral: 'Referral',
|
||
|
|
broker: 'Broker',
|
||
|
|
};
|
||
|
|
|
||
|
|
interface GetColumnsOptions {
|
||
|
|
portSlug: string;
|
||
|
|
onEdit: (interest: InterestRow) => void;
|
||
|
|
onArchive: (interest: InterestRow) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getInterestColumns({
|
||
|
|
portSlug,
|
||
|
|
onEdit,
|
||
|
|
onArchive,
|
||
|
|
}: GetColumnsOptions): ColumnDef<InterestRow, unknown>[] {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
id: 'clientName',
|
||
|
|
accessorKey: 'clientName',
|
||
|
|
header: 'Client',
|
||
|
|
cell: ({ row }) => (
|
||
|
|
<Link
|
||
|
|
href={`/${portSlug}/clients/${row.original.clientId}`}
|
||
|
|
className="font-medium text-primary hover:underline"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
{row.original.clientName ?? '—'}
|
||
|
|
</Link>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'berthMooringNumber',
|
||
|
|
accessorKey: 'berthMooringNumber',
|
||
|
|
header: 'Berth',
|
||
|
|
cell: ({ row }) => {
|
||
|
|
if (!row.original.berthId || !row.original.berthMooringNumber) {
|
||
|
|
return <span className="text-muted-foreground">—</span>;
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<Link
|
||
|
|
href={`/${portSlug}/berths/${row.original.berthId}`}
|
||
|
|
className="text-primary hover:underline text-sm"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
{row.original.berthMooringNumber}
|
||
|
|
</Link>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'pipelineStage',
|
||
|
|
accessorKey: 'pipelineStage',
|
||
|
|
header: 'Stage',
|
||
|
|
cell: ({ getValue }) => {
|
||
|
|
const stage = getValue() as string;
|
||
|
|
return (
|
||
|
|
<span
|
||
|
|
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${STAGE_COLORS[stage] ?? 'bg-gray-100 text-gray-700'}`}
|
||
|
|
>
|
||
|
|
{STAGE_LABELS[stage] ?? stage}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'leadCategory',
|
||
|
|
accessorKey: 'leadCategory',
|
||
|
|
header: 'Category',
|
||
|
|
cell: ({ getValue }) => {
|
||
|
|
const cat = getValue() as string | null;
|
||
|
|
if (!cat) return <span className="text-muted-foreground">—</span>;
|
||
|
|
return (
|
||
|
|
<Badge variant="outline" className="text-xs capitalize">
|
||
|
|
{CATEGORY_LABELS[cat] ?? cat}
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'source',
|
||
|
|
accessorKey: 'source',
|
||
|
|
header: 'Source',
|
||
|
|
cell: ({ getValue }) => {
|
||
|
|
const source = getValue() as string | null;
|
||
|
|
if (!source) return <span className="text-muted-foreground">—</span>;
|
||
|
|
return (
|
||
|
|
<Badge variant="outline" className="text-xs">
|
||
|
|
{SOURCE_LABELS[source] ?? source}
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'tags',
|
||
|
|
header: 'Tags',
|
||
|
|
enableSorting: false,
|
||
|
|
cell: ({ row }) => {
|
||
|
|
const rowTags = row.original.tags ?? [];
|
||
|
|
if (rowTags.length === 0) return <span className="text-muted-foreground">—</span>;
|
||
|
|
return (
|
||
|
|
<div className="flex flex-wrap gap-1">
|
||
|
|
{rowTags.slice(0, 3).map((tag) => (
|
||
|
|
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
|
||
|
|
))}
|
||
|
|
{rowTags.length > 3 && (
|
||
|
|
<Badge variant="secondary" className="text-xs">
|
||
|
|
+{rowTags.length - 3}
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'createdAt',
|
||
|
|
accessorKey: 'createdAt',
|
||
|
|
header: 'Created',
|
||
|
|
cell: ({ getValue }) => (
|
||
|
|
<span className="text-muted-foreground text-sm">
|
||
|
|
{format(new Date(getValue() as string), 'MMM d, yyyy')}
|
||
|
|
</span>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'actions',
|
||
|
|
header: '',
|
||
|
|
enableSorting: false,
|
||
|
|
size: 48,
|
||
|
|
cell: ({ row }) => (
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon"
|
||
|
|
className="h-7 w-7"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
<MoreHorizontal className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||
|
|
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||
|
|
Edit
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem
|
||
|
|
className="text-destructive"
|
||
|
|
onClick={() => onArchive(row.original)}
|
||
|
|
>
|
||
|
|
<Archive className="mr-2 h-3.5 w-3.5" />
|
||
|
|
Archive
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|