File size: 7,274 Bytes
2a3a36f 698ffee 2a3a36f 698ffee 2a3a36f 698ffee 2a3a36f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | 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';
/** Search CRM contacts and link one to the deal (`PATCH contact_id`). */
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>
);
}
/** Search distinct company names from contacts; sets deal `account_name`. */
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>
);
}
|