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 { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { supabase } from "@/integrations/supabase/client"; import { formatCurrency, formatDate } from "@/lib/format"; import { Search, Wallet, Download, ArrowDownToLine, ArrowUpFromLine } from "lucide-react"; export const Route = createFileRoute("/trust/")({ component: () => ( ), }); interface Entry { id: string; client_id: string; case_id: string | null; entry_date: string; entry_type: "deposit" | "withdrawal"; amount: number; note: string | null; } interface ClientLite { id: string; name: string | null; archived_at: string | null; } interface CaseLite { id: string; case_number: string | null; title: string | null; } function TrustLedgerPage() { const [entries, setEntries] = useState([]); const [clients, setClients] = useState>(new Map()); const [cases, setCases] = useState>(new Map()); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [view, setView] = useState<"all" | "active" | "archived">("active"); useEffect(() => { (async () => { setLoading(true); const [entriesRes, clientsRes, casesRes] = await Promise.all([ supabase .from("trust_ledger_entries") .select("id, client_id, case_id, entry_date, entry_type, amount, note") .order("entry_date", { ascending: false }) .limit(5000), supabase.from("clients").select("id, name, archived_at"), supabase.from("cases").select("id, case_number, title"), ]); setEntries((entriesRes.data ?? []) as Entry[]); const cm = new Map(); ((clientsRes.data ?? []) as ClientLite[]).forEach((c) => cm.set(c.id, c)); setClients(cm); const csm = new Map(); ((casesRes.data ?? []) as CaseLite[]).forEach((c) => csm.set(c.id, c)); setCases(csm); setLoading(false); })(); }, []); // Per-client balances const balancesByClient = useMemo(() => { const m = new Map(); for (const e of entries) { const k = e.client_id; if (!m.has(k)) m.set(k, { client: clients.get(k) ?? null, balance: 0, count: 0, lastDate: null }); const g = m.get(k)!; g.balance += e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount); g.count += 1; if (!g.lastDate || e.entry_date > g.lastDate) g.lastDate = e.entry_date; } return m; }, [entries, clients]); const filteredClients = useMemo(() => { const q = search.trim().toLowerCase(); let arr = Array.from(balancesByClient.entries()).map(([id, v]) => ({ id, ...v })); if (view === "active") arr = arr.filter((r) => !r.client?.archived_at); else if (view === "archived") arr = arr.filter((r) => !!r.client?.archived_at); if (q) { arr = arr.filter((r) => (r.client?.name ?? "").toLowerCase().includes(q)); } return arr.sort((a, b) => (a.client?.name ?? "").localeCompare(b.client?.name ?? "")); }, [balancesByClient, search, view]); const totalBalance = useMemo( () => filteredClients.reduce((s, r) => s + r.balance, 0), [filteredClients], ); const visibleClientIds = useMemo(() => new Set(filteredClients.map((c) => c.id)), [filteredClients]); const recentEntries = useMemo( () => entries.filter((e) => visibleClientIds.has(e.client_id)).slice(0, 100), [entries, visibleClientIds], ); const exportCsv = () => { const rows = entries .filter((e) => visibleClientIds.has(e.client_id)) .map((e) => { const c = clients.get(e.client_id); const cs = e.case_id ? cases.get(e.case_id) : null; return { date: e.entry_date, client: c?.name ?? "", archived: c?.archived_at ? "yes" : "", case_number: cs?.case_number ?? "", case_title: cs?.title ?? "", type: e.entry_type, amount: Number(e.amount).toFixed(2), note: (e.note ?? "").replace(/"/g, '""'), }; }); const headers = ["date", "client", "archived", "case_number", "case_title", "type", "amount", "note"]; const csv = [ headers.join(","), ...rows.map((r) => headers.map((h) => `"${(r as any)[h]}"`).join(",")), ].join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `trust-ledger-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); }; return ( Export CSV } />
Total balance ({view})
{formatCurrency(totalBalance)}
Clients with activity
{filteredClients.length}
Total entries
{entries.length}
setSearch(e.target.value)} placeholder="Search clients…" className="pl-8" />
setView(v as any)}> Active Archived All
{loading ? (

Loading…

) : filteredClients.length === 0 ? (

No trust activity matches.

) : (
Client
Entries
Last activity
Balance
{filteredClients.map((r) => (
{r.client?.name ?? "Unknown client"} {r.client?.archived_at && ( archived )}
{r.count}
{r.lastDate ? formatDate(r.lastDate) : "—"}
{formatCurrency(r.balance)}
))}
)}
{recentEntries.length === 0 ? (

No entries.

) : (
{recentEntries.map((e) => { const c = clients.get(e.client_id); const cs = e.case_id ? cases.get(e.case_id) : null; const isDeposit = e.entry_type === "deposit"; return (
{isDeposit ? : }
{c?.name ?? "Unknown client"} {cs && ( · {cs.case_number ?? cs.title} )} {e.note && · {e.note}}
{formatDate(e.entry_date)}
{isDeposit ? "+" : "−"} {formatCurrency(e.amount)}
); })}
)}
); }