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 { supabase } from "@/integrations/supabase/client"; import { Plus, Search, Archive, ArchiveRestore, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react"; import { formatDate, statusBadgeClass } from "@/lib/format"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { setArchived } from "@/lib/archive"; import { NewCaseDialog } from "@/components/cases/new-case-dialog"; import { formatDistanceToNow } from "date-fns"; type SortKey = "title" | "client" | "status" | "attorney" | "opened_at" | "last_activity"; type SortDir = "asc" | "desc"; export const Route = createFileRoute("/cases/")({ component: () => ( ), }); function CasesList() { const navigate = useNavigate(); const [cases, setCases] = useState([]); const [q, setQ] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [view, setView] = useState<"active" | "archived">("active"); const [newOpen, setNewOpen] = useState(false); const [sortKey, setSortKey] = useState("last_activity"); const [sortDir, setSortDir] = useState("desc"); const load = async () => { const { data: caseRows } = await supabase .from("cases") .select("*, client:clients(name), assignee:profiles!cases_assigned_attorney_id_fkey(full_name, email)") .order("updated_at", { ascending: false }); const baseRows = caseRows ?? []; // Aggregate latest activity from related child tables. const [ tasksRes, timeRes, expensesRes, docsRes, callRes, commentsRes, invoicesRes, eventsRes, ] = await Promise.all([ supabase.from("tasks").select("case_id, updated_at").not("case_id", "is", null).limit(20000), supabase.from("time_entries").select("case_id, updated_at").limit(20000), supabase.from("expenses").select("case_id, updated_at").limit(20000), supabase.from("documents").select("case_id, created_at").limit(20000), supabase.from("call_logs").select("case_id, updated_at").limit(20000), supabase.from("comments").select("case_id, created_at").not("case_id", "is", null).limit(20000), supabase.from("invoices").select("case_id, updated_at").not("case_id", "is", null).limit(20000), supabase.from("events").select("case_id, updated_at").not("case_id", "is", null).limit(20000), ]); const latest: Record = {}; const ingest = (rows: any[] | null | undefined, field: "updated_at" | "created_at") => { for (const r of rows ?? []) { const cid = r.case_id as string | null; const ts = r[field] as string | null; if (!cid || !ts) continue; if (!latest[cid] || ts > latest[cid]) latest[cid] = ts; } }; ingest(tasksRes.data, "updated_at"); ingest(timeRes.data, "updated_at"); ingest(expensesRes.data, "updated_at"); ingest(docsRes.data, "created_at"); ingest(callRes.data, "updated_at"); ingest(commentsRes.data, "created_at"); ingest(invoicesRes.data, "updated_at"); ingest(eventsRes.data, "updated_at"); // Fallback to the case's own updated_at if no child activity found. const enriched = baseRows.map((c: any) => { const childTs = latest[c.id]; const lastActivity = childTs && (!c.updated_at || childTs > c.updated_at) ? childTs : c.updated_at; return { ...c, last_activity: lastActivity }; }); setCases(enriched); }; useEffect(() => { load(); }, []); const visible = cases.filter((c) => view === "archived" ? c.archived_at : !c.archived_at, ); const filtered = visible.filter((c) => { const okStatus = statusFilter === "all" || c.status === statusFilter; const okQ = [c.title, c.case_number, c.client?.name].filter(Boolean).join(" ").toLowerCase().includes(q.toLowerCase()); return okStatus && okQ; }); const sorted = useMemo(() => { const getVal = (c: any): string => { switch (sortKey) { case "title": return (c.title ?? "").toLowerCase(); case "client": return (c.client?.name ?? "").toLowerCase(); case "status": return (c.status ?? "").toLowerCase(); case "attorney": return (c.assignee?.full_name || c.assignee?.email || "").toLowerCase(); case "opened_at": return c.opened_at ?? ""; case "last_activity": return c.last_activity ?? ""; } }; const arr = [...filtered]; arr.sort((a, b) => { const av = getVal(a); const bv = getVal(b); if (av < bv) return sortDir === "asc" ? -1 : 1; if (av > bv) return sortDir === "asc" ? 1 : -1; return 0; }); return arr; }, [filtered, sortKey, sortDir]); const toggleSort = (key: SortKey) => { if (sortKey === key) { setSortDir(sortDir === "asc" ? "desc" : "asc"); } else { setSortKey(key); setSortDir(key === "opened_at" || key === "last_activity" ? "desc" : "asc"); } }; const archivedCount = cases.filter((c) => c.archived_at).length; const activeCount = cases.length - archivedCount; const onArchive = async (id: string, archived: boolean) => { if (await setArchived("cases", id, archived)) load(); }; const SortHeader = ({ label, k, align = "left" }: { label: string; k: SortKey; align?: "left" | "right" }) => { const active = sortKey === k; const Icon = !active ? ArrowUpDown : sortDir === "asc" ? ArrowUp : ArrowDown; return ( ); }; return ( setNewOpen(true)}> New case } />
setView(v as "active" | "archived")}> Active ({activeCount}) Archived ({archivedCount})
setQ(e.target.value)} placeholder="Search cases…" className="pl-9" />
{sorted.length === 0 && ( )} {sorted.map((c) => ( navigate({ to: "/cases/$caseId", params: { caseId: c.id } })} > ))}
{view === "archived" ? "No archived cases." : "No cases."}
e.stopPropagation()} > {c.title}
{c.case_number}
{c.client?.name ?? "—"} {c.status.replace("_", " ")} {c.assignee?.full_name || c.assignee?.email || "—"} {formatDate(c.opened_at)} {c.last_activity ? `${formatDistanceToNow(new Date(c.last_activity))} ago` : "—"} e.stopPropagation()}>
); }