From fa2bc58f87e915beac23d0e7c5e6db2b9a955d5a Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 7 Aug 2026 20:59:30 +0000 Subject: [PATCH] Terminal --- src/routes/_authenticated/receivables.tsx | 124 +++++++++++----------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/src/routes/_authenticated/receivables.tsx b/src/routes/_authenticated/receivables.tsx index 8d44abd..e0160c1 100644 --- a/src/routes/_authenticated/receivables.tsx +++ b/src/routes/_authenticated/receivables.tsx @@ -52,17 +52,22 @@ function ReceivablesPage() { (await supabase.from("campuses").select("id, name").order("name")).data ?? [], }); + // Read through v_billing_detail rather than invoices + students(...). A + // billing admin can see every invoice but no student rows, so the nested + // select returned a null student on every row; the view resolves the name + // through a helper that re-checks entitlement instead. It also excludes void + // invoices and computes is_past_due from status, balance and due date + // together, which is stricter than comparing due_date alone. const { data: invoices } = useQuery({ queryKey: ["receivables-invoices"], enabled: allowed, queryFn: async () => ( await supabase - .from("invoices") + .from("v_billing_detail") .select( - "id, invoice_number, student_id, campus_id, billing_period_start, billing_period_end, due_date, total_cents, amount_paid_cents, balance_due_cents, status, students(first_name, last_name)", + "invoice_id, invoice_number, student_id, student_name, campus_id, billing_period_start, billing_period_end, due_date, total_cents, amount_paid_cents, balance_due_cents, status, is_past_due, days_overdue", ) - .neq("status", "void") .order("due_date") ).data ?? [], }); @@ -73,28 +78,25 @@ function ReceivablesPage() { queryFn: async () => ( await supabase - .from("payments") + .from("v_payment_detail") .select( - "id, amount_cents, method, kind, status, reference_number, effective_date, student_id, void_reason, students(first_name, last_name)", + "payment_id, amount_cents, method, kind, status, reference_number, effective_date, student_id, void_reason, student_name", ) - .order("created_at", { ascending: false }) + .order("received_at", { ascending: false }) .limit(15) ).data ?? [], }); - const today = new Date().toISOString().slice(0, 10); - // Aggregated in the client: invoice volumes here are small, and it keeps the // tiles and the table reading from exactly the same rows. const rows = useMemo(() => { let r = invoices ?? []; if (campusFilter !== "all") r = r.filter((i) => i.campus_id === campusFilter); if (statusFilter === "outstanding") r = r.filter((i) => (i.balance_due_cents ?? 0) > 0); - if (statusFilter === "pastdue") - r = r.filter((i) => (i.balance_due_cents ?? 0) > 0 && (i.due_date ?? "") < today); + if (statusFilter === "pastdue") r = r.filter((i) => i.is_past_due); if (statusFilter === "paid") r = r.filter((i) => (i.balance_due_cents ?? 0) <= 0); return r; - }, [invoices, campusFilter, statusFilter, today]); + }, [invoices, campusFilter, statusFilter]); const totals = useMemo(() => { const scope = @@ -104,16 +106,11 @@ function ReceivablesPage() { const invoiced = scope.reduce((s, i) => s + (i.total_cents ?? 0), 0); const collected = scope.reduce((s, i) => s + (i.amount_paid_cents ?? 0), 0); const unpaid = scope.reduce((s, i) => s + Math.max(i.balance_due_cents ?? 0, 0), 0); - const pastDue = scope - .filter((i) => (i.balance_due_cents ?? 0) > 0 && (i.due_date ?? "") < today) - .reduce((s, i) => s + (i.balance_due_cents ?? 0), 0); - const delinquent = new Set( - scope - .filter((i) => (i.balance_due_cents ?? 0) > 0 && (i.due_date ?? "") < today) - .map((i) => i.student_id), - ).size; + const overdue = scope.filter((i) => i.is_past_due); + const pastDue = overdue.reduce((s, i) => s + (i.balance_due_cents ?? 0), 0); + const delinquent = new Set(overdue.map((i) => i.student_id)).size; return { invoiced, collected, unpaid, pastDue, delinquent }; - }, [invoices, campusFilter, today]); + }, [invoices, campusFilter]); const record = useMutation({ mutationFn: async () => { @@ -252,10 +249,13 @@ function ReceivablesPage() { {rows.map((i) => { - const overdue = (i.balance_due_cents ?? 0) > 0 && (i.due_date ?? "") < today; - const name = i.students ? `${i.students.first_name} ${i.students.last_name}` : "—"; + const overdue = i.is_past_due; + const name = i.student_name ?? "—"; + // Every column of a view is nullable in the generated types, so the + // id is pulled into a const the closure below can narrow on. + const studentId = i.student_id; return ( - + {i.invoice_number} {name} @@ -268,12 +268,12 @@ function ReceivablesPage() { {money(i.balance_due_cents ?? 0)} - {(i.balance_due_cents ?? 0) > 0 && i.student_id && ( + {(i.balance_due_cents ?? 0) > 0 && studentId && ( + )} - {p.status !== "posted" && ( - {p.status} - )} - {p.kind !== "payment" && ( - {p.kind} - )} - - - {money(p.amount_cents)} - {p.status === "posted" && p.kind === "payment" && ( - - )} - - - ))} + + ); + })} {(payments ?? []).length === 0 && (
No payments recorded yet.
)}