Added bulk contact actions
X-Lovable-Edit-ID: edt-a51d186f-0b61-40c5-a548-cf51bfdc31f4 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -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<ContactRecord | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [bulkType, setBulkType] = useState<string>("");
|
||||
|
||||
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 (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
@@ -194,8 +265,50 @@ function ContactsIndex() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected.size > 0 && (
|
||||
<Card className="mb-4 border-primary/50 bg-primary/5">
|
||||
<CardContent className="p-3 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium mr-2">
|
||||
{selected.size} selected
|
||||
</span>
|
||||
<Button size="sm" variant="outline" onClick={() => bulkArchive(view !== "archived")}>
|
||||
{view === "archived" ? <ArchiveRestore className="h-4 w-4 mr-1.5" /> : <Archive className="h-4 w-4 mr-1.5" />}
|
||||
{view === "archived" ? "Restore" : "Archive"}
|
||||
</Button>
|
||||
<Select value={bulkType} onValueChange={bulkChangeType}>
|
||||
<SelectTrigger className="h-8 w-48">
|
||||
<SelectValue placeholder="Change type to…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONTACT_TYPES.map((t) => (
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="destructive" onClick={bulkDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-1.5" /> Delete
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={clearSelection} className="ml-auto">
|
||||
<X className="h-4 w-4 mr-1.5" /> Clear
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{filtered.length > 0 && !loading && (
|
||||
<div className="px-4 py-2 border-b flex items-center gap-3 bg-muted/30">
|
||||
<Checkbox
|
||||
checked={allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false}
|
||||
onCheckedChange={toggleAllVisible}
|
||||
aria-label="Select all visible"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Select all {filtered.length} visible
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<p className="p-6 text-sm text-muted-foreground">Loading…</p>
|
||||
) : 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 (
|
||||
<li key={c.id} className="px-4 py-3 hover:bg-muted/40 transition-colors">
|
||||
<li key={c.id} className={`px-4 py-3 hover:bg-muted/40 transition-colors ${selected.has(c.id) ? "bg-primary/5" : ""}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={selected.has(c.id)}
|
||||
onCheckedChange={() => toggleOne(c.id)}
|
||||
aria-label={`Select ${c.name}`}
|
||||
/>
|
||||
<Link
|
||||
to="/contacts/$contactId"
|
||||
params={{ contactId: c.id }}
|
||||
|
||||
Reference in New Issue
Block a user