'use client'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useQuery } from '@tanstack/react-query'; import { ChevronRight, Users } from 'lucide-react'; import { formatDistanceToNowStrict } from 'date-fns'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { apiFetch } from '@/lib/api/client'; import { stageBadgeClass, stageLabel } from '@/lib/constants'; import { computeUrgencyBadges } from '@/components/interests/urgency'; import type { InterestRow } from '@/components/interests/interest-columns'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { cn } from '@/lib/utils'; interface InterestsResponse { data: InterestRow[]; } const PREVIEW_LIMIT = 5; /** * Top-of-overview pulse for the berth detail page. Lists the active * interested parties with their stage + last activity, so the rep can do * berth-level triage ("who's on this slip and how warm are they?") * without clicking into the Interests tab. * * Borrows from the old Nuxt CRM's BerthDetailsModal "Interested Parties" * pattern but uses the new at-a-glance signals (urgency badges, last * activity). */ export function BerthInterestPulse({ berthId }: { berthId: string }) { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const queryKey = ['interests', { berthId, sort: 'dateLastContact', order: 'desc' }]; const { data, isLoading } = useQuery({ queryKey, queryFn: () => apiFetch( `/api/v1/interests?berthId=${berthId}&limit=10&sort=dateLastContact&order=desc`, ), staleTime: 30_000, }); // Stay in sync with the linked-berths list + add-to-interest dialog. // Each of those flows emits a realtime socket event but does NOT // invalidate this exact query key (it's berth-scoped, theirs are // interest-scoped) — bridge via the invalidation hook. useRealtimeInvalidation({ 'interest:berthLinked': [queryKey], 'interest:berthUnlinked': [queryKey], 'interest:berthLinkUpdated': [queryKey], 'interest:created': [queryKey], 'interest:stageChanged': [queryKey], 'interest:archived': [queryKey], }); const all = data?.data ?? []; const active = all.filter((i) => !i.archivedAt && !i.outcome); const preview = active.slice(0, PREVIEW_LIMIT); const more = active.length - preview.length; if (isLoading) { return ( Interested parties
{[0, 1, 2].map((i) => (
))}
); } if (active.length === 0) { return ( Interested parties

No active interests on this berth.

); } return ( Interested parties {active.length}
    {preview.map((i) => { const lastIso = i.dateLastContact ?? i.updatedAt ?? null; const lastActivity = lastIso ? formatDistanceToNowStrict(new Date(lastIso), { addSuffix: true }) : null; const urgency = computeUrgencyBadges(i); const initials = (i.clientName ?? '?') .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]!.toUpperCase()) .join(''); return (
  • {initials || '?'}
    {i.clientName ?? 'Unknown'} {stageLabel(i.pipelineStage)} {urgency.map((b) => ( {b.label} ))}
    {lastActivity ? (

    Last activity {lastActivity}

    ) : null}
  • ); })}
{more > 0 ? ( View all {active.length} interests → ) : null}
); }