'use client'; import Link from 'next/link'; import { useParams, usePathname } from 'next/navigation'; import type { Route } from 'next'; import { useQuery } from '@tanstack/react-query'; import { ArrowRight, ChevronRight } from 'lucide-react'; import { formatDistanceToNowStrict } from 'date-fns'; import { apiFetch } from '@/lib/api/client'; import { Skeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils'; import { PIPELINE_STAGES, STAGE_BADGE, STAGE_DOT, STAGE_LABELS, safeStage, type PipelineStage, } from '@/components/clients/pipeline-constants'; export interface ClientInterestRow { id: string; pipelineStage: string; archivedAt: string | null; updatedAt: string; dateLastContact: string | null; berthMooringNumber?: string | null; yachtName?: string | null; } interface InterestsResponse { data: ClientInterestRow[]; } export function useClientInterests(clientId: string) { return useQuery({ queryKey: ['interests', { clientId }], queryFn: () => apiFetch(`/api/v1/interests?clientId=${clientId}&limit=50`), }); } export function StageStepper({ current, size = 'sm', }: { current: PipelineStage; size?: 'xs' | 'sm'; }) { const idx = PIPELINE_STAGES.indexOf(current); // Segmented progress bar: each stage is a slice of equal width that // lights up once the interest has reached it. Reads at-a-glance, scales // to any container width, and works with 9 stages without becoming // micro-dots that vanish under cramped layouts. const height = size === 'xs' ? 'h-1' : 'h-1.5'; return (
{PIPELINE_STAGES.map((stage, i) => { const isReached = i <= idx; const isCurrent = i === idx; return (
0 ? 'border-l border-card' : '', )} /> ); })}
); } function pickHighest(interests: ClientInterestRow[]): ClientInterestRow | null { const active = interests.filter((i) => !i.archivedAt); if (active.length === 0) return null; return [...active].sort((a, b) => { const ai = PIPELINE_STAGES.indexOf(safeStage(a.pipelineStage)); const bi = PIPELINE_STAGES.indexOf(safeStage(b.pipelineStage)); if (ai !== bi) return bi - ai; return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); })[0]!; } function lastActivityLabel(interests: ClientInterestRow[]): string | null { const candidates = interests .flatMap((i) => [i.dateLastContact, i.updatedAt]) .filter((v): v is string => Boolean(v)) .map((v) => new Date(v).getTime()) .filter((t) => !Number.isNaN(t)); if (candidates.length === 0) return null; const latest = new Date(Math.max(...candidates)); return `${formatDistanceToNowStrict(latest)} ago`; } interface PipelineSummaryProps { clientId: string; /** * `hero` — single-line pulse for the detail header (highest active stage only). * `panel` — compact list of every active interest, for the Overview tab. */ variant?: 'hero' | 'panel'; } function HeroVariant({ clientId, portSlug }: { clientId: string; portSlug: string }) { const pathname = usePathname(); const { data, isLoading } = useClientInterests(clientId); const interests = data?.data ?? []; const top = pickHighest(interests); const activeCount = interests.filter((i) => !i.archivedAt).length; const activity = lastActivityLabel(interests); const interestsTabHref = `${pathname}?tab=interests` as Route; if (isLoading) { return (
); } if (!top) { return (

No active interests

Start one to begin tracking the sales process.

Start interest
); } const stage = safeStage(top.pipelineStage); const berthLabel = top.berthMooringNumber ? `Berth ${top.berthMooringNumber}` : 'General interest'; const detailsHref = `/${portSlug}/interests/${top.id}` as Route; return (
Sales pipeline {activeCount > 1 ? ( · {activeCount} active ) : null}
{berthLabel} {STAGE_LABELS[stage]}
{activity ? `Last activity ${activity}` : 'No activity recorded'} {activeCount > 1 ? ( View all {activeCount} ) : null}
); } function PanelVariant({ clientId, portSlug }: { clientId: string; portSlug: string }) { const pathname = usePathname(); const { data, isLoading } = useClientInterests(clientId); const interests = (data?.data ?? []).filter((i) => !i.archivedAt); const interestsTabHref = `${pathname}?tab=interests` as Route; if (isLoading) { return (
); } if (interests.length === 0) { return (

No active interests

Start one to begin tracking the sales process.

Start interest
); } const sorted = [...interests].sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), ); return (
Sales pipeline · {interests.length} active Manage
    {sorted.map((i) => { const stage = safeStage(i.pipelineStage); const berthLabel = i.berthMooringNumber ? `Berth ${i.berthMooringNumber}` : 'General interest'; const href = `/${portSlug}/interests/${i.id}` as Route; return (
  • {berthLabel} {STAGE_LABELS[stage]}
  • ); })}
); } export function ClientPipelineSummary({ clientId, variant = 'panel' }: PipelineSummaryProps) { const routeParams = useParams<{ portSlug: string }>(); const portSlug = routeParams?.portSlug ?? ''; return variant === 'hero' ? ( ) : ( ); }