From f74c61c9df98667afb05dc58a78c7a4a58633d7d Mon Sep 17 00:00:00 2001 From: renee-png Date: Tue, 2 Jun 2026 00:54:54 -0400 Subject: [PATCH] Budget vs Actuals: custom actuals comparison date range Add From/To date inputs to the Budget vs Actuals report (Accounting section Reports) so actuals can be compared to the budget over any custom range, independent of the page period preset. Defaults to the report period; drives the actuals queries and CSV/PDF export labels. Co-Authored-By: Claude Opus 4.8 --- .../accounting/AccountingReportsPage.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index c948243..5ac1048 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -1310,6 +1310,13 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe const [budgetId, setBudgetId] = useState(""); useEffect(() => { if (!budgetId && budgets.length) setBudgetId(budgets[0].id); }, [budgets, budgetId]); + // Actuals comparison window — defaults to the report period, but the user can + // choose any custom date range to compare against the budget. + const [actFrom, setActFrom] = useState(from); + const [actTo, setActTo] = useState(to); + useEffect(() => { setActFrom(from); setActTo(to); }, [from, to]); + const actualsLabel = `${actFrom} to ${actTo}`; + const { data: accounts = [] } = useQuery({ queryKey: ["accounts", companyId], enabled: !!companyId, @@ -1323,18 +1330,18 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe }); const { data: actualsData } = useQuery({ - queryKey: ["bva-actuals", companyId, from, to], + queryKey: ["bva-actuals", companyId, actFrom, actTo], enabled: !!companyId, queryFn: async () => { const [inv, exp, txns, billItemsRes] = await Promise.all([ // All issued invoices (accrual — not filtered by payment status) - accounting.from("invoices").select("total,status,issue_date").eq("company_id", companyId).gte("issue_date", from).lte("issue_date", to).not("status", "in", '("void","draft")'), + accounting.from("invoices").select("total,status,issue_date").eq("company_id", companyId).gte("issue_date", actFrom).lte("issue_date", actTo).not("status", "in", '("void","draft")'), // Expenses table (tertiary for expense matching) - accounting.from("expenses").select("amount,category,date").eq("company_id", companyId).gte("date", from).lte("date", to), + accounting.from("expenses").select("amount,category,date").eq("company_id", companyId).gte("date", actFrom).lte("date", actTo), // Transactions with coa_account_id (primary — covers banking + receive-payments flow) - accounting.from("transactions").select("coa_account_id,amount,type").eq("company_id", companyId).gte("date", from).lte("date", to).not("coa_account_id", "is", null), + accounting.from("transactions").select("coa_account_id,amount,type").eq("company_id", companyId).gte("date", actFrom).lte("date", actTo).not("coa_account_id", "is", null), // Bill items joined to bills in range (secondary — covers bills paid via Bills page) - accounting.from("bill_items").select("account_id,amount,bills!inner(issue_date,company_id)").eq("bills.company_id", companyId).gte("bills.issue_date", from).lte("bills.issue_date", to).not("account_id", "is", null), + accounting.from("bill_items").select("account_id,amount,bills!inner(issue_date,company_id)").eq("bills.company_id", companyId).gte("bills.issue_date", actFrom).lte("bills.issue_date", actTo).not("account_id", "is", null), ]); return { invoices: inv.data ?? [], @@ -1452,7 +1459,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe return rows; }, [grouped, groupedOrdered, budgetByAcct, actualByAcct]); - const fileBase = `budget-vs-actuals-${from}-to-${to}`; + const fileBase = `budget-vs-actuals-${actFrom}-to-${actTo}`; const exportCSV = () => { const esc = (s: any) => `"${String(s).replace(/"/g, '""')}"`; @@ -1470,7 +1477,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41); doc.text("Budget vs Actuals", 40, 50); doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(110, 116, 122); - doc.text(`${companyName} · ${rangeLabel}`, 40, 66); + doc.text(`${companyName} · ${actualsLabel}`, 40, 66); autoTable(doc, { startY: 80, head: [["Account", "Budget", "Actual", "Variance", "Variance %"]], @@ -1502,6 +1509,12 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe {budgets.map((b: any) => {b.name} (FY {b.fiscal_year}))} +
+ + setActFrom(e.target.value)} className="h-9 w-40" /> + + setActTo(e.target.value)} className="h-9 w-40" /> +