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 { supabase } from "@/integrations/supabase/client"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { formatCurrency, formatDate } from "@/lib/format"; import { ChevronRight, CalendarClock, Search, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react"; export const Route = createFileRoute("/payment-plans/")({ component: PaymentPlansIndexPage, }); function PaymentPlansIndexPage() { const [rows, setRows] = useState([]); const [installments, setInstallments] = useState>({}); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("active"); const [freqFilter, setFreqFilter] = useState("all"); const [sortKey, setSortKey] = useState< "name" | "case" | "frequency" | "next" | "progress" | "total" | "remaining" | "started" >("started"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const toggleSort = (key: typeof sortKey) => { if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc")); else { setSortKey(key); setSortDir("asc"); } }; const load = async () => { setLoading(true); const { data: plans } = await supabase .from("payment_plans") .select( "id, name, status, total_amount, down_payment, installment_count, installment_amount, frequency, start_date, created_at, case_id, collection_id, case:cases(id, case_number, title, client:clients(id, name)), collection:collections(id, case_id, homeowner:homeowners(id, first_name, last_name, unit_number), case:cases(id, case_number, title, client:clients(id, name)))", ) .order("created_at", { ascending: false }); const list = plans ?? []; setRows(list); if (list.length) { const ids = list.map((p: any) => p.id); const { data: inst } = await supabase .from("payment_plan_installments") .select("plan_id, amount, paid, paid_amount, due_date") .in("plan_id", ids) .order("due_date", { ascending: true }); const map: Record = {}; (inst ?? []).forEach((i: any) => { (map[i.plan_id] ||= []).push(i); }); setInstallments(map); } setLoading(false); }; useEffect(() => { load(); }, []); const computed = useMemo(() => { return rows.map((p: any) => { const ins = installments[p.id] ?? []; const total = Number(p.total_amount) || 0; const paidAmt = ins.reduce((s, i) => s + (i.paid ? Number(i.paid_amount ?? i.amount) || 0 : 0), 0); const remaining = total - paidAmt; const paidCount = ins.filter((i) => i.paid).length; const next = ins.find((i) => !i.paid); const homeowner = p.collection?.homeowner; const client = p.collection?.case?.client?.name ?? p.case?.client?.name ?? null; const caseRow = p.collection?.case ?? p.case ?? null; const displayName = p.name || (homeowner ? `${homeowner.last_name}, ${homeowner.first_name}` : null) || (caseRow ? `${caseRow.case_number} · ${caseRow.title}` : "Plan"); const linkTo: { to: any; params: any } = p.collection_id ? { to: "/collections/$collectionId", params: { collectionId: p.collection_id } } : { to: "/cases/$caseId", params: { caseId: p.case_id } }; return { plan: p, ins, total, paidAmt, remaining, paidCount, next, homeowner, client, caseRow, displayName, linkTo }; }); }, [rows, installments]); const filtered = computed.filter((r) => { if (statusFilter !== "all" && r.plan.status !== statusFilter) return false; if (freqFilter !== "all" && r.plan.frequency !== freqFilter) return false; if (!search.trim()) return true; const q = search.toLowerCase(); return ( (r.displayName ?? "").toLowerCase().includes(q) || (r.client ?? "").toLowerCase().includes(q) || (r.caseRow?.case_number ?? "").toLowerCase().includes(q) || (r.caseRow?.title ?? "").toLowerCase().includes(q) ); }); const sorted = useMemo(() => { const getVal = (r: typeof filtered[number]): string | number => { switch (sortKey) { case "name": return (r.displayName ?? "").toLowerCase(); case "case": return (r.client ?? "").toLowerCase(); case "frequency": return r.plan.frequency ?? ""; case "next": return r.next?.due_date ? new Date(r.next.due_date).getTime() : Infinity; case "progress": return r.plan.installment_count ? r.paidCount / r.plan.installment_count : 0; case "total": return r.total; case "remaining": return r.remaining; case "started": return r.plan.start_date ? new Date(r.plan.start_date).getTime() : 0; } }; return [...filtered].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; }); }, [filtered, sortKey, sortDir]); const SortHeader = ({ k, label, className }: { k: typeof sortKey; label: string; className?: string }) => { const Icon = sortKey !== k ? ArrowUpDown : sortDir === "asc" ? ArrowUp : ArrowDown; return ( ); }; return (
setSearch(e.target.value)} className="pl-9" />
{loading ? (
Loading…
) : sorted.length === 0 ? (
No payment plans found.
) : ( {sorted.map((r) => ( {r.displayName} {r.homeowner?.unit_number && ( · Unit {r.homeowner.unit_number} )} {r.plan.status !== "active" && ( {r.plan.status} )}
{r.client ?? "—"}
{r.caseRow && (
{r.caseRow.case_number} · {r.caseRow.title}
)}
{r.plan.frequency} {r.paidCount} / {r.plan.installment_count} {r.next ? formatDate(r.next.due_date) : "—"} {formatCurrency(r.total)} 0 ? "text-destructive font-semibold" : "text-emerald-600" }`} > {formatCurrency(Math.max(0, r.remaining))}
))}
)}
); }