EMAILOUT / frontend /src /components /workspace /DealLinkSearch.jsx
Seth
update
698ffee
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>
);
}