diff --git a/src/routes/contacts.index.tsx b/src/routes/contacts.index.tsx index d0bc23f..3fd9e7d 100644 --- a/src/routes/contacts.index.tsx +++ b/src/routes/contacts.index.tsx @@ -7,10 +7,12 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; -import { Plus, Search, Mail, Phone, Building2, ChevronRight, Edit, Trash2, Briefcase, Users, Archive, ArchiveRestore } from "lucide-react"; +import { Plus, Search, Mail, Phone, Building2, ChevronRight, Edit, Trash2, Briefcase, Users, Archive, ArchiveRestore, X } from "lucide-react"; import { ContactFormDialog, CONTACT_TYPES, type ContactRecord } from "@/components/contacts/contact-form-dialog"; import { setArchived } from "@/lib/archive"; @@ -39,6 +41,8 @@ function ContactsIndex() { const [view, setView] = useState<"active" | "archived">("active"); const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); + const [selected, setSelected] = useState>(new Set()); + const [bulkType, setBulkType] = useState(""); const load = async () => { setLoading(true); @@ -132,6 +136,73 @@ function ContactsIndex() { setOpen(true); }; + const toggleOne = (id: string) => { + setSelected((s) => { + const next = new Set(s); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + const toggleAllVisible = () => { + const visibleIds = filtered.map((r) => r.id); + const allSelected = visibleIds.every((id) => selected.has(id)); + setSelected((s) => { + const next = new Set(s); + if (allSelected) visibleIds.forEach((id) => next.delete(id)); + else visibleIds.forEach((id) => next.add(id)); + return next; + }); + }; + const clearSelection = () => setSelected(new Set()); + + const bulkArchive = async (archived: boolean) => { + const ids = Array.from(selected); + if (!ids.length) return; + const patch = { archived_at: archived ? new Date().toISOString() : null }; + const { error } = await (supabase.from("contacts") as any).update(patch).in("id", ids); + if (error) { + toast.error(error.message); + return; + } + toast.success(`${ids.length} contact${ids.length === 1 ? "" : "s"} ${archived ? "archived" : "restored"}`); + clearSelection(); + load(); + }; + + const bulkDelete = async () => { + const ids = Array.from(selected); + if (!ids.length) return; + if (!confirm(`Delete ${ids.length} contact${ids.length === 1 ? "" : "s"}? This will also remove all case and client links.`)) return; + const { error } = await supabase.from("contacts").delete().in("id", ids); + if (error) { + toast.error(error.message); + return; + } + toast.success(`${ids.length} contact${ids.length === 1 ? "" : "s"} deleted`); + clearSelection(); + load(); + }; + + const bulkChangeType = async (newType: string) => { + const ids = Array.from(selected); + if (!ids.length || !newType) return; + const { error } = await supabase.from("contacts").update({ contact_type: newType }).in("id", ids); + if (error) { + toast.error(error.message); + return; + } + const label = CONTACT_TYPES.find((t) => t.value === newType)?.label ?? newType; + toast.success(`${ids.length} contact${ids.length === 1 ? "" : "s"} changed to ${label}`); + setBulkType(""); + clearSelection(); + load(); + }; + + const visibleIds = filtered.map((r) => r.id); + const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id)); + const someVisibleSelected = visibleIds.some((id) => selected.has(id)); + return ( + {selected.size > 0 && ( + + + + {selected.size} selected + + + + + + + + )} + + {filtered.length > 0 && !loading && ( +
+ + + Select all {filtered.length} visible + +
+ )} {loading ? (

Loading…

) : filtered.length === 0 ? ( @@ -214,8 +327,13 @@ function ContactsIndex() { const typeLabel = CONTACT_TYPES.find((t) => t.value === c.contact_type)?.label ?? c.contact_type; const canEdit = isAdmin || c.created_by === user?.id; return ( -
  • +
  • + toggleOne(c.id)} + aria-label={`Select ${c.name}`} + />