Align BudgetTable on the income-statement standard shipped for the compare/trend reports: sections now read Income -> Expense -> Transfer (useBudget's TYPE_ORDER and BudgetTable's typeOrder both flipped), and the previously unlabeled grand-total row is replaced by two interleaved result rows (Result before transfers / Net result), computed by a new pure, tested module (budgetTableResults.ts) mirroring compareResults.ts's shape. Roll-up covers the previous-year-actual, budgeted-annual, and budgeted-monthly columns alike. All categories remain displayed (the grid stays a full edit surface) -- no collapse, no empty-row toggle, matching this issue's frozen decisions. Resolves #278
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
import type { BudgetYearRow } from "../../shared/types";
|
||
|
||
export type BudgetSectionType = "income" | "expense" | "transfer";
|
||
|
||
/**
|
||
* Sign multiplier applied to budgeted (months/annual) magnitudes. Budgeted
|
||
* amounts are entered/stored as positive magnitudes regardless of type and
|
||
* flipped for display by category type — mirrors `BudgetTable`'s `signFor`.
|
||
*/
|
||
export function signForBudgetType(type: BudgetSectionType): 1 | -1 {
|
||
return type === "expense" ? -1 : 1;
|
||
}
|
||
|
||
/**
|
||
* Aggregate of the budget grid's 3 comparable column groups, already signed:
|
||
* 12 monthly (budgeted) totals, the annual (budgeted) total, and the
|
||
* previous-year (actual) total.
|
||
*/
|
||
export interface BudgetTotals {
|
||
/** index 0-11 = Jan-Dec, signed budgeted total */
|
||
months: number[];
|
||
/** signed budgeted annual total */
|
||
annual: number;
|
||
/** signed actual total from the previous year (already signed in the DB) */
|
||
previousYearTotal: number;
|
||
}
|
||
|
||
function zeroTotals(): BudgetTotals {
|
||
return { months: Array(12).fill(0) as number[], annual: 0, previousYearTotal: 0 };
|
||
}
|
||
|
||
/**
|
||
* Sum every non-subtotal (leaf) row of a single type into one `BudgetTotals`.
|
||
*
|
||
* The type's sign is applied to the budgeted figures (`months`/`annual`),
|
||
* matching how `BudgetTable` displays them. `previousYearTotal` is NOT
|
||
* re-signed: it comes from `getActualTotalsForYear` (a plain signed
|
||
* `SUM(amount)` over the transactions table), already negative for expenses
|
||
* and ~0-net for balanced transfers — the same convention `compareResults.ts`
|
||
* relies on for its own previous-period figures.
|
||
*/
|
||
export function sumLeavesForType(rows: BudgetYearRow[], type: BudgetSectionType): BudgetTotals {
|
||
const sign = signForBudgetType(type);
|
||
const totals = zeroTotals();
|
||
for (const row of rows) {
|
||
if (row.is_parent || row.category_type !== type) continue;
|
||
for (let m = 0; m < 12; m++) totals.months[m] += row.months[m] * sign;
|
||
totals.annual += row.annual * sign;
|
||
totals.previousYearTotal += row.previousYearTotal;
|
||
}
|
||
return totals;
|
||
}
|
||
|
||
/** Component-wise sum of two BudgetTotals (both already signed). */
|
||
function combine(a: BudgetTotals, b: BudgetTotals): BudgetTotals {
|
||
return {
|
||
months: a.months.map((v, i) => v + b.months[i]),
|
||
annual: a.annual + b.annual,
|
||
previousYearTotal: a.previousYearTotal + b.previousYearTotal,
|
||
};
|
||
}
|
||
|
||
export interface BudgetResults {
|
||
income: BudgetTotals;
|
||
expense: BudgetTotals;
|
||
transfer: BudgetTotals;
|
||
/** Budgeted/actual revenues − expenses: the operating result, before transfers. */
|
||
resultBefore: BudgetTotals;
|
||
/** resultBefore + transfers: the bottom-line total. */
|
||
resultNet: BudgetTotals;
|
||
hasTransfers: boolean;
|
||
}
|
||
|
||
/**
|
||
* Income-statement roll-up for the budget grid (Issue #278), mirroring
|
||
* `compareResults.ts`'s shape (see that module's doc comment for the sign
|
||
* convention this relies on).
|
||
*
|
||
* Operates on the flat `BudgetYearRow[]` the grid already renders (produced
|
||
* by `useBudget`) — LEAVES only (`is_parent === false`), so parent subtotal
|
||
* rows are never double-counted. All three column groups (previous-year
|
||
* actual, budgeted annual, budgeted monthly) are folded into the same result
|
||
* rows so "Résultat avant transferts" / "Résultat net" read consistently
|
||
* across every column of the grid.
|
||
*/
|
||
export function computeBudgetResults(rows: BudgetYearRow[]): BudgetResults {
|
||
const income = sumLeavesForType(rows, "income");
|
||
const expense = sumLeavesForType(rows, "expense");
|
||
const transfer = sumLeavesForType(rows, "transfer");
|
||
const resultBefore = combine(income, expense);
|
||
const resultNet = combine(resultBefore, transfer);
|
||
const hasTransfers = rows.some((r) => !r.is_parent && r.category_type === "transfer");
|
||
return { income, expense, transfer, resultBefore, resultNet, hasTransfers };
|
||
}
|