Added board member contacts

X-Lovable-Edit-ID: edt-d29c7018-46ec-400b-9d4f-69447470283c
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 17:16:04 +00:00
co-authored by renee-png
3 changed files with 135 additions and 4 deletions
+20 -3
View File
@@ -12,6 +12,7 @@ import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Loader2, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { ContactPickerPopover } from "./contact-picker-popover";
const boardMember = z.object({
name: z.string().trim().min(1, "Required").max(120),
@@ -299,9 +300,25 @@ export function ClientFormDialog({
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium">Board members</label>
<Button type="button" variant="outline" size="sm" onClick={addBoardMember}>
<Plus className="h-3 w-3 mr-1" /> Add member
</Button>
<div className="flex gap-2">
<ContactPickerPopover
label="From contacts"
onPick={(c) =>
form.setValue("board_members", [
...board,
{
name: c.name,
role: c.title ?? "",
email: c.email ?? "",
phone: c.phone ?? "",
},
])
}
/>
<Button type="button" variant="outline" size="sm" onClick={addBoardMember}>
<Plus className="h-3 w-3 mr-1" /> Add member
</Button>
</div>
</div>
<div className="space-y-2">
{board.length === 0 && (
@@ -0,0 +1,111 @@
import { useEffect, useMemo, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { supabase } from "@/integrations/supabase/client";
import { Search, UserSearch } from "lucide-react";
interface PickResult {
id: string;
name: string;
email: string | null;
phone: string | null;
title: string | null;
company: string | null;
contact_type: string;
}
/**
* Compact "pick from contacts" popover used inside list-style editors
* (e.g. board members rows in the client form).
*/
export function ContactPickerPopover({
onPick,
label = "From contacts",
}: {
onPick: (c: PickResult) => void;
label?: string;
}) {
const [open, setOpen] = useState(false);
const [q, setQ] = useState("");
const [rows, setRows] = useState<PickResult[]>([]);
useEffect(() => {
if (!open) return;
setQ("");
supabase
.from("contacts")
.select("id, name, email, phone, title, company, contact_type")
.is("archived_at", null)
.order("name")
.limit(200)
.then(({ data }) => setRows((data ?? []) as PickResult[]));
}, [open]);
const filtered = useMemo(() => {
const term = q.trim().toLowerCase();
if (!term) return rows;
return rows.filter(
(r) =>
r.name.toLowerCase().includes(term) ||
(r.company ?? "").toLowerCase().includes(term) ||
(r.email ?? "").toLowerCase().includes(term),
);
}, [rows, q]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button type="button" variant="outline" size="sm">
<UserSearch className="h-3.5 w-3.5 mr-1.5" />
{label}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
autoFocus
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search contacts…"
className="h-8 pl-8 text-sm"
/>
</div>
</div>
<div className="max-h-72 overflow-auto">
{filtered.length === 0 ? (
<p className="text-xs text-muted-foreground p-3">No matching contacts.</p>
) : (
<ul className="divide-y">
{filtered.map((c) => (
<li key={c.id}>
<button
type="button"
onClick={() => {
onPick(c);
setOpen(false);
}}
className="w-full text-left px-3 py-2 hover:bg-muted/60 flex items-center justify-between gap-2"
>
<div className="min-w-0">
<div className="text-sm font-medium truncate">{c.name}</div>
<div className="text-[11px] text-muted-foreground truncate">
{[c.company, c.email].filter(Boolean).join(" · ") || "—"}
</div>
</div>
<Badge variant="outline" className="text-[10px] shrink-0 capitalize">
{c.contact_type}
</Badge>
</button>
</li>
))}
</ul>
)}
</div>
</PopoverContent>
</Popover>
);
}
@@ -89,9 +89,12 @@ export function ContactsLinkTab({
<CardContent className="p-5">
<div className="flex items-center justify-between mb-4">
<div>
<div className="text-xs uppercase tracking-wider text-muted-foreground font-medium">Contacts</div>
<div className="text-xs uppercase tracking-wider text-muted-foreground font-medium">
{parentTable === "client_contacts" ? "Additional contacts" : "Contacts"}
</div>
<div className="text-sm text-muted-foreground">
{linked.length} linked contact{linked.length === 1 ? "" : "s"}
{parentTable === "client_contacts" && " — accountants, vendors, board liaisons, etc."}
</div>
</div>
<div className="flex gap-2">