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 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 00:54:54 -04:00
parent 7a7435a8ee
commit f74c61c9df
+20 -7
View File
@@ -1310,6 +1310,13 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
const [budgetId, setBudgetId] = useState<string>("");
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) => <SelectItem key={b.id} value={b.id}>{b.name} (FY {b.fiscal_year})</SelectItem>)}
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<Label className="text-sm">Actuals from</Label>
<Input type="date" value={actFrom} onChange={(e) => setActFrom(e.target.value)} className="h-9 w-40" />
<Label className="text-sm">to</Label>
<Input type="date" value={actTo} onChange={(e) => setActTo(e.target.value)} className="h-9 w-40" />
</div>
<div className="ml-auto flex gap-2">
<Button variant="outline" onClick={exportCSV}><Download className="mr-1 h-4 w-4" /> CSV</Button>
<Button onClick={exportPDF}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>