'use client'; import { useState } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import type { Route } from 'next'; import { useQuery } from '@tanstack/react-query'; import { format, formatDistanceToNowStrict } from 'date-fns'; import { ArrowRight, CheckCircle2, ChevronRight, Circle, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { EmptyState } from '@/components/shared/empty-state'; import { Skeleton } from '@/components/ui/skeleton'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { apiFetch } from '@/lib/api/client'; import { PIPELINE_STAGES, type PipelineStage } from '@/lib/constants'; import { cn } from '@/lib/utils'; import { STAGE_BADGE, STAGE_LABELS, safeStage } from '@/components/clients/pipeline-constants'; import { StageStepper, useClientInterests, type ClientInterestRow, } from '@/components/clients/client-pipeline-summary'; import { InterestForm } from '@/components/interests/interest-form'; const LEAD_CATEGORY_LABELS: Record = { general_interest: 'General interest', specific_qualified: 'Specific qualified', hot_lead: 'Hot lead', }; function InterestRowItem({ interest, onOpen, }: { interest: ClientInterestRow; onOpen: (i: ClientInterestRow) => void; }) { const stage = safeStage(interest.pipelineStage); const berthLabel = interest.berthMooringNumber ? `Berth ${interest.berthMooringNumber}` : 'General interest'; const yachtLabel = interest.yachtName ?? null; return ( // Tap opens a right-side Sheet preview rather than navigating to the // full interest page. The sheet covers ~80% of interactions ("what // stage is this at, when did we last touch it"). For deeper edits // the sheet has an "Open full page" CTA. ); } function lastActivityFor(interest: ClientInterestRow): string | null { const candidates = [interest.dateLastContact, interest.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; return `${formatDistanceToNowStrict(new Date(Math.max(...candidates)))} ago`; } /** Full interest record returned by `/api/v1/interests/[id]`. Only the fields * the preview sheet actually reads are typed here; the API returns more. */ interface InterestDetail { id: string; pipelineStage: string; leadCategory: string | null; source: string | null; notes: string | null; dateLastContact: string | null; dateEoiSent: string | null; dateEoiSigned: string | null; dateDepositReceived: string | null; dateContractSent: string | null; dateContractSigned: string | null; } function useInterestDetail(id: string | null) { return useQuery<{ data: InterestDetail }>({ queryKey: ['interest-detail-preview', id], queryFn: () => apiFetch<{ data: InterestDetail }>(`/api/v1/interests/${id}`), enabled: id !== null, // Detail rarely changes during a single drawer-open session; stale-time // keeps re-opens snappy without preventing background refetch. staleTime: 30_000, }); } /** Format a date-only or ISO timestamp as e.g. "Apr 8, 2026". Returns null for * empty input so callers can render an "empty" state. */ function formatDate(value: string | null | undefined): string | null { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return format(d, 'MMM d, yyyy'); } /** A single milestone row inside the preview sheet's milestone summary. Filled * circle when the step is done, hollow when pending. Trailing meta line * shows the date stamp or a "pending" hint. */ function MilestoneRow({ label, done, date, hint = 'pending', }: { label: string; done: boolean; date: string | null; hint?: string; }) { return (
  • {done ? ( ) : ( )} {label} {date ?? hint}
  • ); } /** * Right-side sheet preview of a single interest. "Tap an interest → see * what's happening without leaving the client page". Shows the pipeline * progress, a compact milestone summary (EOI / Deposit / Contract), * lead context, last contact, and a notes teaser. Tap-out / Esc * dismisses; the full edit page is one tap away via "Open full page →". */ function InterestPreviewSheet({ interest, portSlug, onClose, }: { interest: ClientInterestRow | null; portSlug: string; onClose: () => void; }) { // Pin the most recently selected interest so the sheet stays populated // during the close-animation tail (Radix keeps the content mounted // through the slide-out). Conditional setState is safe here - the // guard ensures it only fires when the prop actually changes. const [pinned, setPinned] = useState(interest); if (interest && interest !== pinned) setPinned(interest); const showing = pinned; const detail = useInterestDetail(showing?.id ?? null); const fullDetail = detail.data?.data ?? null; const open = interest !== null; const stage = showing ? safeStage(showing.pipelineStage) : null; const stageIdx = stage ? PIPELINE_STAGES.indexOf(stage) : -1; const reached = (target: PipelineStage) => stageIdx !== -1 && PIPELINE_STAGES.indexOf(target) <= stageIdx; const berthLabel = showing ? showing.berthMooringNumber ? `Berth ${showing.berthMooringNumber}` : 'General interest' : ''; const yachtLabel = showing?.yachtName ?? null; const activity = showing ? lastActivityFor(showing) : null; const fullHref = showing ? (`/${portSlug}/interests/${showing.id}` as Route) : ('/' as Route); const leadLabel = fullDetail?.leadCategory ? (LEAD_CATEGORY_LABELS[fullDetail.leadCategory] ?? fullDetail.leadCategory) : null; const sourceLabel = fullDetail?.source ? fullDetail.source.replace(/\b\w/g, (m) => m.toUpperCase()) : null; const lastContactDate = formatDate(fullDetail?.dateLastContact); const notesPreview = fullDetail?.notes?.trim() || null; return ( { if (!next) onClose(); }} >
    {berthLabel} {yachtLabel ? (

    {yachtLabel}

    ) : null}
    {stage ? ( {STAGE_LABELS[stage]} ) : null}
    {/* Pipeline-stepper segmented bar - the same primitive used on the row card, so the at-a-glance progress hint is consistent across surfaces. */} {stage ? (

    Pipeline progress

    ) : null} {/* Milestones - three sections matching the full interest detail page (EOI / Deposit / Contract). Done-state is derived from the pipeline stage so seed data without per-step dates still renders correctly. The full milestone columns + per-step actions live behind "Open full page". */}

    Milestones

    EOI

    Deposit

    Contract

    {/* Compact key/value pairs - lead category, source, last contact, activity. Each row collapses cleanly when its value is missing so the drawer scales from sparse seed data to full records without empty placeholders. */}
    {leadLabel ? ( <>
    Lead
    {leadLabel}
    ) : null} {sourceLabel ? ( <>
    Source
    {sourceLabel}
    ) : null} {lastContactDate ? ( <>
    Last contact
    {lastContactDate}
    ) : null} {activity ? ( <>
    Last activity
    {activity}
    ) : null}
    {notesPreview ? (

    Notes

    {notesPreview}

    ) : null}
    ); } function InterestSkeleton() { return (
    ); } interface ClientInterestsTabProps { clientId: string; } export function ClientInterestsTab({ clientId }: ClientInterestsTabProps) { const routeParams = useParams<{ portSlug: string }>(); const portSlug = routeParams?.portSlug ?? ''; const [createOpen, setCreateOpen] = useState(false); const [previewInterest, setPreviewInterest] = useState(null); const { data, isLoading, isError } = useClientInterests(clientId); if (isLoading) { return (
    ); } if (isError) { return

    Could not load interests for this client.

    ; } const interests = data?.data ?? []; if (interests.length === 0) { return ( <> setCreateOpen(true), }} /> ); } const active = interests.filter((i) => !i.archivedAt); const archived = interests.filter((i) => i.archivedAt); return (
    {active.length > 0 ? (
    {active.map((i) => ( ))}
    ) : null} {archived.length > 0 ? (

    Archived

    {archived.map((i) => ( ))}
    ) : null} setPreviewInterest(null)} />
    ); }