From 3f0c7ba1bb421366a42cc13a8c5c0a0af9e956a9 Mon Sep 17 00:00:00 2001 From: renee-png Date: Thu, 18 Jun 2026 23:04:47 -0400 Subject: [PATCH] Reserve Workbook: add Reserve Spending tab (auto-imported, real-time) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the page in tabs: Workbook (assumptions/components/projection) and a new Reserve Spending tab. The spending tab auto-imports every GL line on reserve-flagged (is_reserve) expense accounts — total + reserve balance, spending-by-year, and a full imported-expenditure detail list (date, account, description, ref, amount). It shares the live GL query so it feeds the projection's actual expenses in real time; Re-import button refreshes. Co-Authored-By: Claude Opus 4.8 --- src/pages/ReserveWorkbookPage.tsx | 116 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/src/pages/ReserveWorkbookPage.tsx b/src/pages/ReserveWorkbookPage.tsx index 8d85c92..2b69ae0 100644 --- a/src/pages/ReserveWorkbookPage.tsx +++ b/src/pages/ReserveWorkbookPage.tsx @@ -8,12 +8,13 @@ import { useAssociation } from "@/contexts/AssociationContext"; import { supabase } from "@/integrations/supabase/client"; import { accounting } from "@/lib/accountingClient"; import { useCompanyId } from "@/pages/accounting/lib/useCompanyId"; -import { money } from "@/pages/accounting/lib/format"; +import { money, fmtDate } from "@/pages/accounting/lib/format"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const PROJ_YEARS = 30; const PRELIM_NOTE = "These projections are for preliminary purposes only and are not subject to annual increases or inflation."; @@ -34,7 +35,7 @@ async function fetchReserveGL(cid: string, ids: string[], from: string, to: stri const PAGE = 1000; const out: any[] = []; for (let offset = 0; ; offset += PAGE) { const { data, error } = await accounting.from("journal_entry_lines") - .select("debit,credit,account_id,journal_entries!inner(company_id,date)") + .select("debit,credit,account_id,journal_entries!inner(company_id,date,description,reference)") .eq("journal_entries.company_id", cid).in("account_id", ids) .gte("journal_entries.date", from).lte("journal_entries.date", to) .range(offset, offset + PAGE - 1); @@ -137,6 +138,26 @@ export default function ReserveWorkbookPage() { return out; }, [reserveGL, acctType]); + // Detailed reserve expenditures (each GL line on a reserve EXPENSE account), + // newest first — drives the "Reserve Spending" tab. Net = debit - credit. + const acctName = useMemo(() => new Map((reserveAccts as any[]).map((a) => [a.id, `${a.code ? a.code + " " : ""}${a.name}`])), [reserveAccts]); + const reserveSpending = useMemo(() => { + const rows = (reserveGL as any[]) + .filter((l) => acctType.get(l.account_id) === "expense") + .map((l) => ({ + date: l.journal_entries?.date ?? "", + account: acctName.get(l.account_id) ?? "—", + description: l.journal_entries?.description ?? "", + reference: l.journal_entries?.reference ?? "", + amount: (Number(l.debit) || 0) - (Number(l.credit) || 0), + })) + .filter((r) => Math.abs(r.amount) > 0.004) + .sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); + const total = rows.reduce((s, r) => s + r.amount, 0); + const byYear = Object.entries(actualExpByYear).map(([y, amt]) => ({ year: Number(y), amount: amt })).sort((a, b) => b.year - a.year); + return { rows, total, byYear }; + }, [reserveGL, acctType, acctName, actualExpByYear]); + // --- Per-component computed reserve-study values --- const inflFrac = num(inflation) / 100; const compCalc = (c: Comp) => { @@ -300,7 +321,13 @@ export default function ReserveWorkbookPage() { ) : companyLoading ? (
) : ( - <> + + + Workbook + Reserve Spending {reserveSpending.rows.length > 0 ? `(${reserveSpending.rows.length})` : ""} + + +
setFy(Number(e.target.value) || fy)} />
@@ -417,7 +444,88 @@ export default function ReserveWorkbookPage() {

Expenses use scheduled component replacements; years with a • include actual reserve expenditures loaded from the GL.

- +
+ + {/* ── Reserve Spending: auto-imported from the GL, real-time ── */} + + + +
+
Total Reserve Spending (imported)
+
{money(reserveSpending.total)}
+
+
+
Reserve Balance (GL)
+
{money(reserveFundBalance)}
+
+
+ +
+
+
+ +
+ Reserve spending is auto-imported from every reserve-flagged (is_reserve) expense account in the GL — it flows into the projection's actual expenses in real time. Flag accounts as reserves in the Chart of Accounts to include them here. +
+ + {/* By year */} + + Spending by Year + + + + + + + {reserveSpending.byYear.length === 0 ? ( + + ) : reserveSpending.byYear.map((r) => ( + + + + + ))} + {reserveSpending.byYear.length > 0 && ( + + + + + )} + +
YearReserve Expenditures
No reserve expenditures found. Flag reserve expense accounts as is_reserve in the Chart of Accounts.
{r.year}{money(r.amount)}
Total{money(reserveSpending.total)}
+
+
+ + {/* Detail */} + + Imported Expenditures ({reserveSpending.rows.length}) + + + + + + + + + {reserveSpending.rows.length === 0 ? ( + + ) : reserveSpending.rows.map((r, i) => ( + + + + + + + + ))} + +
DateAccountDescriptionRefAmount
No reserve expenditures imported.
{r.date ? fmtDate(r.date) : "—"}{r.account}{r.description || "—"}{r.reference || "—"}{money(r.amount)}
+
+
+
+
)} );