mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
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:
@@ -1310,6 +1310,13 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
const [budgetId, setBudgetId] = useState<string>("");
|
const [budgetId, setBudgetId] = useState<string>("");
|
||||||
useEffect(() => { if (!budgetId && budgets.length) setBudgetId(budgets[0].id); }, [budgets, budgetId]);
|
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({
|
const { data: accounts = [] } = useQuery({
|
||||||
queryKey: ["accounts", companyId],
|
queryKey: ["accounts", companyId],
|
||||||
enabled: !!companyId,
|
enabled: !!companyId,
|
||||||
@@ -1323,18 +1330,18 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: actualsData } = useQuery({
|
const { data: actualsData } = useQuery({
|
||||||
queryKey: ["bva-actuals", companyId, from, to],
|
queryKey: ["bva-actuals", companyId, actFrom, actTo],
|
||||||
enabled: !!companyId,
|
enabled: !!companyId,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const [inv, exp, txns, billItemsRes] = await Promise.all([
|
const [inv, exp, txns, billItemsRes] = await Promise.all([
|
||||||
// All issued invoices (accrual — not filtered by payment status)
|
// 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)
|
// 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)
|
// 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)
|
// 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 {
|
return {
|
||||||
invoices: inv.data ?? [],
|
invoices: inv.data ?? [],
|
||||||
@@ -1452,7 +1459,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
return rows;
|
return rows;
|
||||||
}, [grouped, groupedOrdered, budgetByAcct, actualByAcct]);
|
}, [grouped, groupedOrdered, budgetByAcct, actualByAcct]);
|
||||||
|
|
||||||
const fileBase = `budget-vs-actuals-${from}-to-${to}`;
|
const fileBase = `budget-vs-actuals-${actFrom}-to-${actTo}`;
|
||||||
|
|
||||||
const exportCSV = () => {
|
const exportCSV = () => {
|
||||||
const esc = (s: any) => `"${String(s).replace(/"/g, '""')}"`;
|
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.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41);
|
||||||
doc.text("Budget vs Actuals", 40, 50);
|
doc.text("Budget vs Actuals", 40, 50);
|
||||||
doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(110, 116, 122);
|
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, {
|
autoTable(doc, {
|
||||||
startY: 80,
|
startY: 80,
|
||||||
head: [["Account", "Budget", "Actual", "Variance", "Variance %"]],
|
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>)}
|
{budgets.map((b: any) => <SelectItem key={b.id} value={b.id}>{b.name} (FY {b.fiscal_year})</SelectItem>)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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">
|
<div className="ml-auto flex gap-2">
|
||||||
<Button variant="outline" onClick={exportCSV}><Download className="mr-1 h-4 w-4" /> CSV</Button>
|
<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>
|
<Button onClick={exportPDF}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user