126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { Archive, MoreHorizontal, Pencil } from 'lucide-react';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from '@/components/ui/dropdown-menu';
|
||
|
|
import { TagBadge } from '@/components/shared/tag-badge';
|
||
|
|
import {
|
||
|
|
ListCard,
|
||
|
|
ListCardAvatar,
|
||
|
|
ListCardMeta,
|
||
|
|
deriveInitials,
|
||
|
|
} from '@/components/shared/list-card';
|
||
|
|
import { getCountryName } from '@/lib/i18n/countries';
|
||
|
|
import type { ClientRow } from './client-columns';
|
||
|
|
|
||
|
|
const SOURCE_LABELS: Record<string, string> = {
|
||
|
|
website: 'Website',
|
||
|
|
manual: 'Manual',
|
||
|
|
referral: 'Referral',
|
||
|
|
broker: 'Broker',
|
||
|
|
};
|
||
|
|
|
||
|
|
interface ClientCardProps {
|
||
|
|
client: ClientRow;
|
||
|
|
portSlug: string;
|
||
|
|
onEdit: (client: ClientRow) => void;
|
||
|
|
onArchive: (client: ClientRow) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ClientCard({ client, portSlug, onEdit, onArchive }: ClientCardProps) {
|
||
|
|
const primary = client.contacts?.find((c) => c.isPrimary);
|
||
|
|
const nationality = client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null;
|
||
|
|
const sourceLabel = client.source ? (SOURCE_LABELS[client.source] ?? client.source) : null;
|
||
|
|
const yachtCount = client.yachtCount ?? 0;
|
||
|
|
const companyCount = client.companyCount ?? 0;
|
||
|
|
const tags = client.tags ?? [];
|
||
|
|
|
||
|
|
const meta = [nationality, sourceLabel].filter(Boolean) as string[];
|
||
|
|
const counts: string[] = [];
|
||
|
|
if (yachtCount > 0) counts.push(`${yachtCount} ${yachtCount === 1 ? 'yacht' : 'yachts'}`);
|
||
|
|
if (companyCount > 0)
|
||
|
|
counts.push(`${companyCount} ${companyCount === 1 ? 'company' : 'companies'}`);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ListCard
|
||
|
|
href={`/${portSlug}/clients/${client.id}`}
|
||
|
|
ariaLabel={`Client ${client.fullName}`}
|
||
|
|
actions={
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon"
|
||
|
|
className="h-9 w-9"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
aria-label={`Actions for ${client.fullName}`}
|
||
|
|
>
|
||
|
|
<MoreHorizontal className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem onClick={() => onEdit(client)}>
|
||
|
|
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||
|
|
Edit
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(client)}>
|
||
|
|
<Archive className="mr-2 h-3.5 w-3.5" />
|
||
|
|
Archive
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
<ListCardAvatar initials={deriveInitials(client.fullName)} />
|
||
|
|
<div className="min-w-0 flex-1">
|
||
|
|
<div className="flex items-start justify-between gap-2">
|
||
|
|
<h3 className="truncate text-base font-semibold tracking-tight text-foreground">
|
||
|
|
{client.fullName}
|
||
|
|
</h3>
|
||
|
|
<span aria-hidden className="block h-9 w-9 shrink-0" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{primary ? (
|
||
|
|
<p className="truncate text-sm text-muted-foreground">{primary.value}</p>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
{meta.length > 0 ? (
|
||
|
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted-foreground">
|
||
|
|
{meta.map((m, i) => (
|
||
|
|
<span key={m} className="inline-flex items-center gap-1">
|
||
|
|
{i > 0 ? <span aria-hidden>·</span> : null}
|
||
|
|
<ListCardMeta>{m}</ListCardMeta>
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
{counts.length > 0 ? (
|
||
|
|
<p className="mt-0.5 text-xs text-muted-foreground">{counts.join(' · ')}</p>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
{tags.length > 0 ? (
|
||
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
||
|
|
{tags.slice(0, 2).map((tag) => (
|
||
|
|
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
|
||
|
|
))}
|
||
|
|
{tags.length > 2 ? (
|
||
|
|
<span className="inline-flex items-center rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||
|
|
+{tags.length - 2}
|
||
|
|
</span>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</ListCard>
|
||
|
|
);
|
||
|
|
}
|