diff --git a/src/routes/contacts.index.tsx b/src/routes/contacts.index.tsx index 4d342b0..67c830f 100644 --- a/src/routes/contacts.index.tsx +++ b/src/routes/contacts.index.tsx @@ -14,6 +14,21 @@ import { Plus, Search, Mail, Phone, Building2, ChevronRight, Edit, Trash2, Brief import { ContactFormDialog, CONTACT_TYPES, type ContactRecord } from "@/components/contacts/contact-form-dialog"; import { setArchived } from "@/lib/archive"; +type Category = "all" | "companies" | "people" | "lawyers"; + +const CATEGORY_TYPES: Record, string[]> = { + lawyers: ["opposing_counsel", "co_counsel"], + companies: ["vendor", "court"], + people: ["expert", "witness", "process_server", "client_contact", "other"], +}; + +function categoryOf(type: string | null | undefined): Exclude { + const t = type ?? "other"; + if (CATEGORY_TYPES.lawyers.includes(t)) return "lawyers"; + if (CATEGORY_TYPES.companies.includes(t)) return "companies"; + return "people"; +} + export const Route = createFileRoute("/contacts/")({ component: () => ( @@ -37,6 +52,7 @@ function ContactsIndex() { const [q, setQ] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [view, setView] = useState<"active" | "archived">("active"); + const [category, setCategory] = useState("all"); const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); @@ -70,6 +86,7 @@ function ContactsIndex() { const term = q.trim().toLowerCase(); return rows.filter((r) => { if (view === "archived" ? !r.archived_at : !!r.archived_at) return false; + if (category !== "all" && categoryOf(r.contact_type) !== category) return false; if (typeFilter !== "all" && r.contact_type !== typeFilter) return false; if (!term) return true; return ( @@ -79,11 +96,36 @@ function ContactsIndex() { (r.phone ?? "").toLowerCase().includes(term) ); }); - }, [rows, q, typeFilter, view]); + }, [rows, q, typeFilter, view, category]); + + const grouped = useMemo(() => { + const map = new Map(); + for (const r of filtered) { + const ch = (r.name?.[0] ?? "#").toUpperCase(); + const letter = /[A-Z]/.test(ch) ? ch : "#"; + if (!map.has(letter)) map.set(letter, []); + map.get(letter)!.push(r); + } + return Array.from(map.entries()).sort(([a], [b]) => { + if (a === "#") return 1; + if (b === "#") return -1; + return a.localeCompare(b); + }); + }, [filtered]); const archivedCount = rows.filter((r) => r.archived_at).length; const activeCount = rows.length - archivedCount; + const categoryCounts = useMemo(() => { + const base = rows.filter((r) => (view === "archived" ? !!r.archived_at : !r.archived_at)); + return { + all: base.length, + companies: base.filter((r) => categoryOf(r.contact_type) === "companies").length, + people: base.filter((r) => categoryOf(r.contact_type) === "people").length, + lawyers: base.filter((r) => categoryOf(r.contact_type) === "lawyers").length, + }; + }, [rows, view]); + const remove = async (id: string, name: string) => { if (!confirm(`Delete contact "${name}"? This will also remove all case and client links.`)) return; const { error } = await supabase.from("contacts").delete().eq("id", id); @@ -150,6 +192,15 @@ function ContactsIndex() { + setCategory(v as Category)} className="mb-4"> + + All ({categoryCounts.all}) + Companies ({categoryCounts.companies}) + People ({categoryCounts.people}) + Lawyers ({categoryCounts.lawyers}) + + + {loading ? ( @@ -159,8 +210,14 @@ function ContactsIndex() { {rows.length === 0 ? "No contacts yet." : view === "archived" ? "No archived contacts." : "No contacts match your filters."}

) : ( -
    - {filtered.map((c) => { +
    + {grouped.map(([letter, items]) => ( +
    +
    + {letter} +
    +
      + {items.map((c) => { const typeLabel = CONTACT_TYPES.find((t) => t.value === c.contact_type)?.label ?? c.contact_type; const canEdit = isAdmin || c.created_by === user?.id; return ( @@ -213,7 +270,10 @@ function ContactsIndex() { ); })} -
    +
+ + ))} + )}