Switched contact picker to search

X-Lovable-Edit-ID: edt-1ce6f37f-fa65-4862-8c34-40ff4bb96b8e
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 20:12:20 +00:00
co-authored by renee-png
+116 -31
View File
@@ -22,7 +22,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Phone, Loader2 } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Phone, Loader2, Search } from "lucide-react";
import { toast } from "sonner";
interface ClientOpt {
@@ -48,7 +49,6 @@ export function QuickAddCallLog() {
const [clients, setClients] = useState<ClientOpt[]>([]);
const [cases, setCases] = useState<CaseOpt[]>([]);
const [contacts, setContacts] = useState<ContactOpt[]>([]);
const [clientId, setClientId] = useState("");
const [caseId, setCaseId] = useState("");
@@ -63,25 +63,54 @@ export function QuickAddCallLog() {
useEffect(() => {
if (!open) return;
(async () => {
const [{ data: cs }, { data: ks }, { data: ct }] = await Promise.all([
const [{ data: cs }, { data: ks }] = await Promise.all([
supabase.from("clients").select("id, name").is("archived_at", null).order("name"),
supabase
.from("cases")
.select("id, case_number, title, client_id")
.is("archived_at", null)
.order("case_number", { ascending: false }),
supabase
.from("contacts")
.select("id, name, phone")
.is("archived_at", null)
.order("name"),
]);
setClients(cs ?? []);
setCases((ks ?? []) as CaseOpt[]);
setContacts((ct ?? []) as ContactOpt[]);
})();
}, [open]);
// Server-side contact search (debounced). No preloaded list — every keystroke queries the DB.
const [contactQuery, setContactQuery] = useState("");
const [contactResults, setContactResults] = useState<ContactOpt[]>([]);
const [contactSearching, setContactSearching] = useState(false);
const [selectedContact, setSelectedContact] = useState<ContactOpt | null>(null);
const [contactPopoverOpen, setContactPopoverOpen] = useState(false);
useEffect(() => {
if (!open) return;
const term = contactQuery.trim();
setContactSearching(true);
let cancelled = false;
const handle = setTimeout(async () => {
let q = supabase
.from("contacts")
.select("id, name, phone")
.is("archived_at", null)
.order("name")
.limit(25);
if (term) {
const esc = term.replace(/[%,]/g, " ");
q = q.or(`name.ilike.%${esc}%,phone.ilike.%${esc}%,email.ilike.%${esc}%,company.ilike.%${esc}%`);
}
const { data } = await q;
if (!cancelled) {
setContactResults((data ?? []) as ContactOpt[]);
setContactSearching(false);
}
}, 200);
return () => {
cancelled = true;
clearTimeout(handle);
};
}, [open, contactQuery]);
const filteredCases = useMemo(
() => (clientId ? cases.filter((k) => k.client_id === clientId) : cases),
[cases, clientId],
@@ -91,6 +120,9 @@ export function QuickAddCallLog() {
setClientId("");
setCaseId("");
setContactId("");
setSelectedContact(null);
setContactQuery("");
setContactResults([]);
setCallerName("");
setCallerPhone("");
setDirection("outbound");
@@ -218,28 +250,81 @@ export function QuickAddCallLog() {
<div>
<Label>Contact (optional)</Label>
<SearchableSelect
value={contactId}
onValueChange={(id) => {
setContactId(id);
const c = contacts.find((x) => x.id === id);
if (c) {
if (!callerName) setCallerName(c.name);
if (!callerPhone && c.phone) setCallerPhone(c.phone);
}
}}
placeholder="Select contact or enter manually below"
searchPlaceholder="Search contacts…"
emptyText="No contacts found."
options={[
{ value: "", label: "— None —", keywords: "none" },
...contacts.map((c) => ({
value: c.id,
label: c.phone ? `${c.name} (${c.phone})` : c.name,
keywords: `${c.name} ${c.phone ?? ""}`,
})),
]}
/>
<Popover open={contactPopoverOpen} onOpenChange={setContactPopoverOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
className="w-full justify-between font-normal"
>
{selectedContact ? (
<span className="truncate">
{selectedContact.name}
{selectedContact.phone ? ` (${selectedContact.phone})` : ""}
</span>
) : (
<span className="text-muted-foreground">Search contacts…</span>
)}
<Search className="h-3.5 w-3.5 ml-2 opacity-50 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
<div className="p-2 border-b">
<Input
autoFocus
value={contactQuery}
onChange={(e) => setContactQuery(e.target.value)}
placeholder="Type to search all contacts…"
className="h-8 text-sm"
/>
</div>
<div className="max-h-72 overflow-auto">
{selectedContact && (
<button
type="button"
onClick={() => {
setSelectedContact(null);
setContactId("");
setContactPopoverOpen(false);
}}
className="w-full text-left px-3 py-2 text-xs text-muted-foreground hover:bg-muted/60 border-b"
>
Clear selection
</button>
)}
{contactSearching ? (
<p className="text-xs text-muted-foreground p-3">Searching…</p>
) : contactResults.length === 0 ? (
<p className="text-xs text-muted-foreground p-3">
{contactQuery.trim() ? "No matching contacts." : "Start typing to search."}
</p>
) : (
<ul className="divide-y">
{contactResults.map((c) => (
<li key={c.id}>
<button
type="button"
onClick={() => {
setContactId(c.id);
setSelectedContact(c);
if (!callerName) setCallerName(c.name);
if (!callerPhone && c.phone) setCallerPhone(c.phone);
setContactPopoverOpen(false);
}}
className="w-full text-left px-3 py-2 hover:bg-muted/60"
>
<div className="text-sm font-medium truncate">{c.name}</div>
{c.phone && (
<div className="text-[11px] text-muted-foreground truncate">{c.phone}</div>
)}
</button>
</li>
))}
</ul>
)}
</div>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-3">