From 17558a264d96fc850cb0e19a6c5d7e056ae617d9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:19:52 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/clients.index.tsx | 145 ++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 21 deletions(-) diff --git a/src/routes/clients.index.tsx b/src/routes/clients.index.tsx index eab25b6..b945d3c 100644 --- a/src/routes/clients.index.tsx +++ b/src/routes/clients.index.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Button } from "@/components/ui/button"; @@ -7,11 +7,13 @@ import { Card, CardContent } from "@/components/ui/card"; 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 { SearchableSelect } from "@/components/ui/searchable-select"; import { supabase } from "@/integrations/supabase/client"; import { ClientFormDialog } from "@/components/clients/client-form-dialog"; import { Plus, Search, Building2, User, Archive, ArchiveRestore } from "lucide-react"; import { formatDate } from "@/lib/format"; -import { setArchived } from "@/lib/archive"; +import { toast } from "sonner"; export const Route = createFileRoute("/clients/")({ component: () => ( @@ -24,43 +26,96 @@ export const Route = createFileRoute("/clients/")({ function ClientsList() { const navigate = useNavigate(); const [clients, setClients] = useState([]); + const [users, setUsers] = useState([]); const [q, setQ] = useState(""); const [open, setOpen] = useState(false); const [view, setView] = useState<"active" | "archived">("active"); + const [selected, setSelected] = useState>(new Set()); + const [assignTo, setAssignTo] = useState(""); + const [busy, setBusy] = useState(false); const load = async () => { - const { data } = await supabase - .from("clients") - .select("*") - .order("name", { ascending: true }); - setClients(data ?? []); + const [{ data: cli }, { data: usr }] = await Promise.all([ + supabase.from("clients").select("*").order("name", { ascending: true }), + supabase.from("profiles").select("id, full_name, email").order("full_name", { ascending: true }), + ]); + setClients(cli ?? []); + setUsers(usr ?? []); }; - useEffect(() => { - load(); - }, []); + useEffect(() => { load(); }, []); - const visible = clients.filter((c) => - view === "archived" ? c.archived_at : !c.archived_at, - ); + const visible = clients.filter((c) => (view === "archived" ? c.archived_at : !c.archived_at)); const filtered = visible.filter((c) => [c.name, c.management_company, c.primary_contact_name, c.primary_contact_email] - .filter(Boolean) - .join(" ") - .toLowerCase() - .includes(q.toLowerCase()), + .filter(Boolean).join(" ").toLowerCase().includes(q.toLowerCase()), ); const archivedCount = clients.filter((c) => c.archived_at).length; const activeCount = clients.length - archivedCount; + const typeIcon = (t: string) => (t === "hoa" || t === "condo" ? Building2 : User); - const typeIcon = (t: string) => - t === "hoa" || t === "condo" ? Building2 : User; + // Selection helpers — clear when switching views/search. + useEffect(() => { setSelected(new Set()); }, [view]); + const visibleIds = useMemo(() => filtered.map((c) => c.id), [filtered]); + const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id)); + const someSelected = selected.size > 0 && !allSelected; + + const toggleAll = () => { + if (allSelected) setSelected(new Set()); + else setSelected(new Set(visibleIds)); + }; + const toggleOne = (id: string) => { + const next = new Set(selected); + if (next.has(id)) next.delete(id); else next.add(id); + setSelected(next); + }; const onArchive = async (id: string, archived: boolean) => { - if (await setArchived("clients", id, archived)) load(); + const patch = { archived_at: archived ? new Date().toISOString() : null }; + const { error } = await supabase.from("clients").update(patch).eq("id", id); + if (error) toast.error(error.message); + else { toast.success(archived ? "Client archived" : "Client restored"); load(); } }; + const bulkArchive = async (archived: boolean) => { + if (selected.size === 0) return; + setBusy(true); + const ids = Array.from(selected); + const { error } = await supabase + .from("clients") + .update({ archived_at: archived ? new Date().toISOString() : null }) + .in("id", ids); + setBusy(false); + if (error) toast.error(error.message); + else { + toast.success(`${ids.length} client(s) ${archived ? "archived" : "restored"}`); + setSelected(new Set()); + load(); + } + }; + + const bulkAssign = async () => { + if (selected.size === 0 || !assignTo) return; + setBusy(true); + const ids = Array.from(selected); + const { error } = await supabase.from("clients").update({ created_by: assignTo }).in("id", ids); + setBusy(false); + if (error) toast.error(error.message); + else { + toast.success(`${ids.length} client(s) reassigned`); + setSelected(new Set()); + setAssignTo(""); + load(); + } + }; + + const userOptions = users.map((u) => ({ + value: u.id, + label: u.full_name || u.email || u.id, + keywords: `${u.full_name || ""} ${u.email || ""}`, + })); + return ( + {selected.size > 0 && ( +
+ {selected.size} selected + +
+ {view === "active" ? ( + + ) : ( + + )} +
+ Assign to: +
+ +
+ +
+ )} + + @@ -108,7 +203,7 @@ function ClientsList() { {filtered.length === 0 && ( - navigate({ to: "/clients/$clientId", params: { clientId: c.id } })} > +
+ + Name Type Contact
+ {visible.length === 0 ? view === "archived" ? "No archived clients." @@ -119,12 +214,20 @@ function ClientsList() { )} {filtered.map((c) => { const Icon = typeIcon(c.client_type); + const isChecked = selected.has(c.id); return (
e.stopPropagation()}> + toggleOne(c.id)} + aria-label={`Select ${c.name}`} + /> +