import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; 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"; 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 { toast } from "sonner"; export const Route = createFileRoute("/clients/")({ component: () => ( ), }); 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 () => { // Page through clients so we never silently hit Supabase's 1000-row cap. const pageSize = 1000; const all: any[] = []; for (let from = 0; ; from += pageSize) { const { data, error } = await supabase .from("clients") .select("*") .order("name", { ascending: true }) .range(from, from + pageSize - 1); if (error) { toast.error(error.message); break; } const batch = data ?? []; all.push(...batch); if (batch.length < pageSize) break; } const { data: usr } = await supabase .from("profiles") .select("id, full_name, email") .order("full_name", { ascending: true }); setClients(all); setUsers(usr ?? []); }; useEffect(() => { load(); }, []); 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()), ); const archivedCount = clients.filter((c) => c.archived_at).length; const activeCount = clients.length - archivedCount; 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) => { 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 ( setOpen(true)}> New client } />
setView(v as "active" | "archived")}> Active ({activeCount}) Archived ({archivedCount})
setQ(e.target.value)} placeholder="Search by name, contact, manager…" className="pl-9" />
{selected.size > 0 && (
{selected.size} selected
{view === "active" ? ( ) : ( )}
Assign to:
)} {filtered.length === 0 && ( )} {filtered.map((c) => { const Icon = typeIcon(c.client_type); const isChecked = selected.has(c.id); return ( navigate({ to: "/clients/$clientId", params: { clientId: c.id } })} > ); })}
Name Type Contact Units Created
{visible.length === 0 ? view === "archived" ? "No archived clients." : "No clients yet. Add your first one." : "No matches."}
e.stopPropagation()}> toggleOne(c.id)} aria-label={`Select ${c.name}`} /> e.stopPropagation()} > {c.name} {c.management_company && (
{c.management_company}
)}
{c.client_type} {c.primary_contact_name || "—"} {c.primary_contact_email && (
{c.primary_contact_email}
)}
{c.num_units ?? "—"} {formatDate(c.created_at)} e.stopPropagation()}>
); }