'use client'; import { useEffect, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { AlertCircle, ArrowRight, Briefcase, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { apiFetch } from '@/lib/api/client'; import { cn } from '@/lib/utils'; interface MatchData { clientId: string; fullName: string; score: number; confidence: 'high' | 'medium' | 'low'; reasons: string[]; interestCount: number; emails: string[]; phonesE164: string[]; } interface DedupSuggestionPanelProps { /** Free-text inputs from the in-flight new-client form. The panel * debounces them and queries /api/v1/clients/match-candidates. */ email?: string | null; phone?: string | null; name?: string | null; /** Caller wants to attach the new interest to an existing client * rather than creating a new one. The form switches to * interest-only mode and pre-fills the client. */ onUseExisting: (match: MatchData) => void; /** User explicitly said "create new anyway." Hide the panel until * they change input again. */ onDismiss?: () => void; } /** * Surfaces existing clients that match the form's in-flight inputs. * * Renders nothing while inputs are short / no useful match found. * On a high-confidence match, the panel interrupts visually with a * solid border and a primary "Use this client" button. * * Wired into the new-client form. Skipped in edit mode. */ export function DedupSuggestionPanel({ email, phone, name, onUseExisting, onDismiss, }: DedupSuggestionPanelProps) { const [dismissed, setDismissed] = useState(false); // Debounce inputs by 300ms so we don't fire on every keystroke. Keep // the latest debounced values in component state. const [debounced, setDebounced] = useState({ email: email ?? '', phone: phone ?? '', name: name ?? '', }); useEffect(() => { const t = setTimeout(() => { setDebounced({ email: email ?? '', phone: phone ?? '', name: name ?? '' }); // Clear the dismissed flag when inputs change - the user typed // something new, so the prior dismissal no longer applies. setDismissed(false); }, 300); return () => clearTimeout(t); }, [email, phone, name]); const hasSomething = debounced.email.length > 3 || debounced.phone.length > 3 || debounced.name.length > 2; const { data, isFetching } = useQuery<{ data: MatchData[] }>({ queryKey: ['dedup-match-candidates', debounced], queryFn: () => { const params = new URLSearchParams(); if (debounced.email) params.set('email', debounced.email); if (debounced.phone) params.set('phone', debounced.phone); if (debounced.name) params.set('name', debounced.name); return apiFetch<{ data: MatchData[] }>(`/api/v1/clients/match-candidates?${params}`); }, enabled: hasSomething && !dismissed, // Same query is fine to cache for a minute - moves are slow at this layer. staleTime: 60_000, }); if (dismissed) return null; if (!hasSomething) return null; if (isFetching && !data) return null; const matches = data?.data ?? []; if (matches.length === 0) return null; const top = matches[0]!; const isHigh = top.confidence === 'high'; return (
{isHigh ? 'This looks like an existing client' : 'Possible match - check before creating'}
{top.fullName}
{top.confidence}{top.reasons.join(' ยท ')}