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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user