Files
pn-new-crm/src/components/clients/client-columns.tsx

166 lines
4.8 KiB
TypeScript
Raw Normal View History

'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 ClientRow {
id: string;
fullName: string;
nationality: string | null;
source: string | null;
archivedAt: string | null;
createdAt: string;
contacts?: Array<{ channel: string; value: string; isPrimary: boolean }>;
tags?: Array<{ id: string; name: string; color: string }>;
}
const SOURCE_LABELS: Record<string, string> = {
website: 'Website',
manual: 'Manual',
referral: 'Referral',
broker: 'Broker',
};
interface GetColumnsOptions {
portSlug: string;
onEdit: (client: ClientRow) => void;
onArchive: (client: ClientRow) => void;
}
// TODO: Add "Yachts" (count) and "Primary company" columns once the
// GET /api/v1/clients list endpoint joins owned-yachts and primary-company
// data into the row shape. Until then, the columns are omitted rather than
// shown as empty placeholders.
export function getClientColumns({
portSlug,
onEdit,
onArchive,
}: GetColumnsOptions): ColumnDef<ClientRow, unknown>[] {
return [
{
id: 'fullName',
accessorKey: 'fullName',
header: 'Name',
cell: ({ row }) => (
<Link
href={`/${portSlug}/clients/${row.original.id}`}
className="font-medium text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{row.original.fullName}
</Link>
),
},
{
id: 'primaryContact',
header: 'Primary Contact',
enableSorting: false,
cell: ({ row }) => {
const primary = row.original.contacts?.find((c) => c.isPrimary);
if (!primary) return <span className="text-muted-foreground"></span>;
return (
<span className="text-sm">
<span className="text-muted-foreground capitalize">{primary.channel}: </span>
{primary.value}
</span>
);
},
},
{
id: 'nationality',
accessorKey: 'nationality',
header: 'Nationality',
cell: ({ getValue }) => (
<span className="text-muted-foreground">{(getValue() as string | null) ?? '—'}</span>
),
},
{
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="capitalize text-xs">
{SOURCE_LABELS[source] ?? source}
</Badge>
);
},
},
{
id: 'tags',
header: 'Tags',
enableSorting: false,
cell: ({ row }) => {
const clientTags = row.original.tags ?? [];
if (clientTags.length === 0) return <span className="text-muted-foreground"></span>;
return (
<div className="flex flex-wrap gap-1">
{clientTags.slice(0, 3).map((tag) => (
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
))}
{clientTags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{clientTags.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>
),
},
];
}