| import React, { useEffect, useState } from 'react'; |
| import { Loader2, Search, UserPlus, Building2 } from 'lucide-react'; |
| import { Button } from '@/components/ui/button'; |
| import { Input } from '@/components/ui/input'; |
| import { cn } from '@/lib/utils'; |
| import { apiFetch } from '@/lib/api'; |
|
|
| |
| export function DealContactSearch({ linkedContactId, onPatchDeal, className }) { |
| const [q, setQ] = useState(''); |
| const [results, setResults] = useState([]); |
| const [loading, setLoading] = useState(false); |
| const [linking, setLinking] = useState(false); |
|
|
| useEffect(() => { |
| if (!q.trim()) { |
| setResults([]); |
| return; |
| } |
| const t = setTimeout(async () => { |
| setLoading(true); |
| try { |
| const res = await apiFetch( |
| `/api/contacts?search=${encodeURIComponent(q.trim())}&limit=12&sort_by=created_at&sort_dir=desc` |
| ); |
| const data = await res.json().catch(() => ({})); |
| setResults(res.ok ? data.contacts || [] : []); |
| } catch { |
| setResults([]); |
| } finally { |
| setLoading(false); |
| } |
| }, 280); |
| return () => clearTimeout(t); |
| }, [q]); |
|
|
| const pick = async (c) => { |
| setLinking(true); |
| try { |
| await onPatchDeal({ contact_id: c.id }); |
| setQ(''); |
| setResults([]); |
| } finally { |
| setLinking(false); |
| } |
| }; |
|
|
| const unlink = async () => { |
| setLinking(true); |
| try { |
| await onPatchDeal({ contact_id: null }); |
| } finally { |
| setLinking(false); |
| } |
| }; |
|
|
| return ( |
| <div className={cn('rounded-lg border border-slate-200 bg-slate-50/80 p-3 mb-3', className)}> |
| <div className="flex items-center gap-2 text-xs font-medium text-slate-600 mb-2"> |
| <UserPlus className="h-3.5 w-3.5 shrink-0" /> |
| Link contact from CRM |
| </div> |
| <div className="relative"> |
| <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" /> |
| <Input |
| value={q} |
| onChange={(e) => setQ(e.target.value)} |
| placeholder="Search name, email, company…" |
| className="pl-9 text-sm bg-white" |
| disabled={linking} |
| /> |
| {loading ? ( |
| <Loader2 className="absolute right-2.5 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-slate-400" /> |
| ) : null} |
| </div> |
| {results.length > 0 ? ( |
| <ul className="mt-2 max-h-40 overflow-y-auto rounded-md border border-slate-200 bg-white text-sm divide-y divide-slate-100"> |
| {results.map((c) => ( |
| <li key={c.id}> |
| <button |
| type="button" |
| disabled={linking || linkedContactId === c.id} |
| className="w-full text-left px-3 py-2 hover:bg-violet-50 disabled:opacity-50" |
| onClick={() => pick(c)} |
| > |
| <div className="font-medium text-slate-800 truncate"> |
| {[c.first_name, c.last_name].filter(Boolean).join(' ') || '—'} |
| </div> |
| <div className="text-xs text-slate-500 truncate">{c.email || '—'}</div> |
| <div className="text-xs text-slate-400 truncate">{c.company || ''}</div> |
| </button> |
| </li> |
| ))} |
| </ul> |
| ) : null} |
| {linkedContactId ? ( |
| <div className="mt-2 flex justify-end"> |
| <Button type="button" variant="ghost" size="sm" className="text-slate-600" onClick={unlink} disabled={linking}> |
| Remove contact link |
| </Button> |
| </div> |
| ) : null} |
| </div> |
| ); |
| } |
|
|
| |
| export function DealCompanySearch({ onPatchDeal, className }) { |
| const [q, setQ] = useState(''); |
| const [results, setResults] = useState([]); |
| const [loading, setLoading] = useState(false); |
| const [applying, setApplying] = useState(false); |
|
|
| useEffect(() => { |
| if (!q.trim()) { |
| setResults([]); |
| return; |
| } |
| const t = setTimeout(async () => { |
| setLoading(true); |
| try { |
| const res = await apiFetch(`/api/company-names?q=${encodeURIComponent(q.trim())}&limit=20`); |
| const data = await res.json().catch(() => ({})); |
| setResults(res.ok ? data.names || [] : []); |
| } catch { |
| setResults([]); |
| } finally { |
| setLoading(false); |
| } |
| }, 280); |
| return () => clearTimeout(t); |
| }, [q]); |
|
|
| const pick = async (name) => { |
| setApplying(true); |
| try { |
| await onPatchDeal({ account_name: name }); |
| setQ(''); |
| setResults([]); |
| } finally { |
| setApplying(false); |
| } |
| }; |
|
|
| return ( |
| <div className={cn('rounded-lg border border-slate-200 bg-slate-50/80 p-3 mb-3', className)}> |
| <div className="flex items-center gap-2 text-xs font-medium text-slate-600 mb-2"> |
| <Building2 className="h-3.5 w-3.5 shrink-0" /> |
| Add company from CRM (account name) |
| </div> |
| <div className="relative"> |
| <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" /> |
| <Input |
| value={q} |
| onChange={(e) => setQ(e.target.value)} |
| placeholder="Search company names…" |
| className="pl-9 text-sm bg-white" |
| disabled={applying} |
| /> |
| {loading ? ( |
| <Loader2 className="absolute right-2.5 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-slate-400" /> |
| ) : null} |
| </div> |
| {results.length > 0 ? ( |
| <ul className="mt-2 max-h-36 overflow-y-auto rounded-md border border-slate-200 bg-white text-sm divide-y divide-slate-100"> |
| {results.map((name) => ( |
| <li key={name}> |
| <button |
| type="button" |
| disabled={applying} |
| className="w-full text-left px-3 py-2 hover:bg-violet-50" |
| onClick={() => pick(name)} |
| > |
| {name} |
| </button> |
| </li> |
| ))} |
| </ul> |
| ) : null} |
| </div> |
| ); |
| } |
|
|