Files
pn-new-crm/src/components/berths/active-interests-popover.tsx

102 lines
3.9 KiB
TypeScript
Raw Normal View History

'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { Loader2, Star } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { apiFetch } from '@/lib/api/client';
import { stageBadgeClass, stageLabel } from '@/lib/constants';
interface ActiveInterestRow {
interestId: string;
clientName: string;
pipelineStage: string;
isPrimary: boolean;
isInEoiBundle: boolean;
}
interface Props {
berthId: string;
portSlug: string;
count: number;
}
/**
* Click-to-expand popover for the berth-list "Active interests" cell.
* Lazy-loads the linked-interest list when the rep opens it; cached
* for 30s. Each row links to the interest detail page and shows the
* pipeline-stage chip + the primary/EOI-bundle flags so the rep can
* judge urgency without leaving the berth list.
*/
export function ActiveInterestsPopover({ berthId, portSlug, count }: Props) {
// Lazy-load: only fetch when the popover opens. Pattern from the
// detail-label fallback queries elsewhere in the codebase — the
// `enabled` flag flips on first open.
const { data, isLoading, isError } = useQuery<{ data: ActiveInterestRow[] }>({
queryKey: ['berth', berthId, 'active-interests'],
queryFn: () =>
apiFetch<{ data: ActiveInterestRow[] }>(`/api/v1/berths/${berthId}/active-interests`),
staleTime: 30_000,
// The popover only renders when the trigger is clicked, so the
// enabled gate falls out naturally from React Query's behaviour
// inside the conditionally-rendered PopoverContent.
});
if (count === 0) return <span className="text-muted-foreground"></span>;
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="inline-flex items-center gap-1 rounded-sm px-1 font-medium tabular-nums hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
aria-label={`Show ${count} active ${count === 1 ? 'interest' : 'interests'}`}
>
{count}
</button>
</PopoverTrigger>
<PopoverContent side="right" align="start" className="w-72 p-2">
<div className="mb-1 px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Active interests
</div>
{isLoading ? (
<div className="flex items-center gap-2 px-2 py-3 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
Loading
</div>
) : isError ? (
<div className="px-2 py-3 text-sm text-destructive">Failed to load interests.</div>
) : (data?.data ?? []).length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">No active interests.</div>
) : (
<ul className="divide-y">
{(data?.data ?? []).map((row) => (
<li key={row.interestId} className="px-1 py-1.5">
<Link
href={`/${portSlug}/interests/${row.interestId}`}
className="flex items-center justify-between gap-2 rounded-sm px-1 py-0.5 text-sm hover:bg-muted/60"
>
<span className="flex min-w-0 items-center gap-1 truncate">
{row.isPrimary ? (
<Star className="h-3 w-3 shrink-0 text-amber-500" aria-hidden />
) : null}
<span className="truncate">{row.clientName}</span>
</span>
<span
className={`shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${stageBadgeClass(
row.pipelineStage,
)}`}
>
{stageLabel(row.pipelineStage)}
</span>
</Link>
</li>
))}
</ul>
)}
</PopoverContent>
</Popover>
);
}