import { createFileRoute, Link } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; 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 { 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 { ContactFormDialog, CONTACT_TYPES, type ContactRecord } from "@/components/contacts/contact-form-dialog"; import { setArchived } from "@/lib/archive"; export const Route = createFileRoute("/contacts/")({ component: () => ( ), }); interface ContactRow extends Required>, ContactRecord { id: string; created_by: string | null; case_links: number; client_links: number; archived_at: string | null; } function ContactsIndex() { const { user, isAdmin } = useAuth(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [q, setQ] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [view, setView] = useState<"active" | "archived">("active"); const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); const load = async () => { setLoading(true); const [{ data, error }, { data: cc }, { data: clc }] = await Promise.all([ supabase.from("contacts").select("*").order("name"), supabase.from("case_contacts").select("contact_id"), supabase.from("client_contacts").select("contact_id"), ]); if (error) toast.error(error.message); const caseCounts = new Map(); (cc ?? []).forEach((r: any) => caseCounts.set(r.contact_id, (caseCounts.get(r.contact_id) ?? 0) + 1)); const clientCounts = new Map(); (clc ?? []).forEach((r: any) => clientCounts.set(r.contact_id, (clientCounts.get(r.contact_id) ?? 0) + 1)); setRows( (data ?? []).map((c: any) => ({ ...c, case_links: caseCounts.get(c.id) ?? 0, client_links: clientCounts.get(c.id) ?? 0, })), ); setLoading(false); }; useEffect(() => { load(); }, []); const filtered = useMemo(() => { const term = q.trim().toLowerCase(); return rows.filter((r) => { if (view === "archived" ? !r.archived_at : !!r.archived_at) return false; if (typeFilter !== "all" && r.contact_type !== typeFilter) return false; if (!term) return true; return ( r.name.toLowerCase().includes(term) || (r.company ?? "").toLowerCase().includes(term) || (r.email ?? "").toLowerCase().includes(term) || (r.phone ?? "").toLowerCase().includes(term) ); }); }, [rows, q, typeFilter, view]); 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 typeCounts = useMemo(() => { const base = rows.filter((r) => (view === "archived" ? !!r.archived_at : !r.archived_at)); const counts: Record = { all: base.length }; for (const t of CONTACT_TYPES) { counts[t.value] = base.filter((r) => (r.contact_type ?? "other") === t.value).length; } return counts; }, [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); if (error) { toast.error(error.message); return; } toast.success("Contact deleted"); setRows((r) => r.filter((x) => x.id !== id)); }; const onArchive = async (id: string, archived: boolean) => { if (await setArchived("contacts", id, archived)) load(); }; const startNew = () => { setEditing(null); setOpen(true); }; const startEdit = (c: ContactRow) => { setEditing(c); setOpen(true); }; return ( New contact } /> setView(v as "active" | "archived")}> Active ({activeCount}) Archived ({archivedCount})
setQ(e.target.value)} className="pl-9" />
{CONTACT_TYPES.map((t) => ( ))}
{loading ? (

Loading…

) : filtered.length === 0 ? (

{rows.length === 0 ? "No contacts yet." : view === "archived" ? "No archived contacts." : "No contacts match your filters."}

) : (
{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 (
  • {c.name} {typeLabel} {c.title && {c.title}}
    {c.company && {c.company}} {c.email && {c.email}} {c.phone && {c.phone}}
    {c.case_links > 0 && {c.case_links} case{c.case_links === 1 ? "" : "s"}} {c.client_links > 0 && {c.client_links} client{c.client_links === 1 ? "" : "s"}}
    {canEdit && ( <> )}
  • ); })}
))}
)}
load()} />
); }