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."}
) : (
-