mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Budget vs Actuals: show full per-period budget, no proration
The Budget column overlap-weighted partial months, so a YTD range ending mid-month showed a fractional budget. Now include each budget period's full amount whenever the selected window touches it, for both the main and comparison windows — exactly as entered on the budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2000,16 +2000,16 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
|
|
||||||
const selectedBudget = useMemo(() => (budgets as any[]).find((b) => b.id === budgetId), [budgets, budgetId]);
|
const selectedBudget = useMemo(() => (budgets as any[]).find((b) => b.id === budgetId), [budgets, budgetId]);
|
||||||
|
|
||||||
// Budget pro-rated to the selected actuals window (B2): sum each budget period
|
// Budget for the selected actuals window: include each budget period's FULL
|
||||||
// weighted by how much of it overlaps [actFrom, actTo]. period_index maps to
|
// amount, exactly as entered, for any period the window touches — no proration.
|
||||||
// months (monthly), quarters (quarterly), or the whole year (annual).
|
// period_index maps to months (monthly), quarters (quarterly), or the whole
|
||||||
|
// year (annual). E.g. Jan 1 – Jun 16 = full Jan…Jun monthly budgets.
|
||||||
const budgetByAcct = useMemo(() => {
|
const budgetByAcct = useMemo(() => {
|
||||||
const m: Record<string, number> = {};
|
const m: Record<string, number> = {};
|
||||||
const pt = String(selectedBudget?.period_type ?? "annual");
|
const pt = String(selectedBudget?.period_type ?? "annual");
|
||||||
const fy = Number(selectedBudget?.fiscal_year) || new Date(actFrom || actTo || Date.now()).getFullYear();
|
const fy = Number(selectedBudget?.fiscal_year) || new Date(actFrom || actTo || Date.now()).getFullYear();
|
||||||
const fromT = actFrom ? new Date(actFrom).getTime() : -Infinity;
|
const fromT = actFrom ? new Date(actFrom).getTime() : -Infinity;
|
||||||
const toT = actTo ? new Date(actTo).getTime() : Infinity;
|
const toT = actTo ? new Date(actTo).getTime() : Infinity;
|
||||||
const DAY = 86400000;
|
|
||||||
const span = (idx: number): [number, number] => {
|
const span = (idx: number): [number, number] => {
|
||||||
if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
|
if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
|
||||||
if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
|
if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
|
||||||
@@ -2017,11 +2017,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
};
|
};
|
||||||
for (const e of (entries as any[])) {
|
for (const e of (entries as any[])) {
|
||||||
const [s, en] = span(Number(e.period_index) || 0);
|
const [s, en] = span(Number(e.period_index) || 0);
|
||||||
const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT));
|
if (s > toT || en < fromT) continue; // period not touched by the window
|
||||||
const full = en - s;
|
m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount);
|
||||||
const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1;
|
|
||||||
if (weight <= 0) continue;
|
|
||||||
m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight;
|
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}, [entries, selectedBudget, actFrom, actTo]);
|
}, [entries, selectedBudget, actFrom, actTo]);
|
||||||
@@ -2029,7 +2026,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
const actualByAcct = useMemo(() => computeBvaActuals(actualsData), [actualsData]);
|
const actualByAcct = useMemo(() => computeBvaActuals(actualsData), [actualsData]);
|
||||||
const cmpActualByAcct = useMemo(() => computeBvaActuals(cmpActualsData), [cmpActualsData]);
|
const cmpActualByAcct = useMemo(() => computeBvaActuals(cmpActualsData), [cmpActualsData]);
|
||||||
|
|
||||||
// Comparison-window budget (pro-rated like budgetByAcct, over [cmpFrom, cmpTo]).
|
// Comparison-window budget (full per-period amounts like budgetByAcct, over
|
||||||
|
// [cmpFrom, cmpTo]). No proration.
|
||||||
const cmpBudgetByAcct = useMemo(() => {
|
const cmpBudgetByAcct = useMemo(() => {
|
||||||
const m: Record<string, number> = {};
|
const m: Record<string, number> = {};
|
||||||
if (!cmpOn || !cmpFrom || !cmpTo) return m;
|
if (!cmpOn || !cmpFrom || !cmpTo) return m;
|
||||||
@@ -2037,7 +2035,6 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
const fy = Number(selectedBudget?.fiscal_year) || new Date(cmpFrom || cmpTo || Date.now()).getFullYear();
|
const fy = Number(selectedBudget?.fiscal_year) || new Date(cmpFrom || cmpTo || Date.now()).getFullYear();
|
||||||
const fromT = new Date(cmpFrom).getTime();
|
const fromT = new Date(cmpFrom).getTime();
|
||||||
const toT = new Date(cmpTo).getTime();
|
const toT = new Date(cmpTo).getTime();
|
||||||
const DAY = 86400000;
|
|
||||||
const span = (idx: number): [number, number] => {
|
const span = (idx: number): [number, number] => {
|
||||||
if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
|
if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
|
||||||
if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
|
if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
|
||||||
@@ -2045,11 +2042,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
};
|
};
|
||||||
for (const e of (entries as any[])) {
|
for (const e of (entries as any[])) {
|
||||||
const [s, en] = span(Number(e.period_index) || 0);
|
const [s, en] = span(Number(e.period_index) || 0);
|
||||||
const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT));
|
if (s > toT || en < fromT) continue; // period not touched by the window
|
||||||
const full = en - s;
|
m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount);
|
||||||
const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1;
|
|
||||||
if (weight <= 0) continue;
|
|
||||||
m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight;
|
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}, [entries, selectedBudget, cmpOn, cmpFrom, cmpTo]);
|
}, [entries, selectedBudget, cmpOn, cmpFrom, cmpTo]);
|
||||||
@@ -2194,7 +2188,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full text-xs text-muted-foreground">
|
<div className="w-full text-xs text-muted-foreground">
|
||||||
Budget pro-rated to the selected period ({String((selectedBudget as any)?.period_type ?? "annual")} budget).
|
Budget shows the full {String((selectedBudget as any)?.period_type ?? "annual")} amounts for every period the selected range touches — exactly as entered, no proration.
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user