Simpl-Resultat/src/components/budget/budgetTableResults.ts
le king fu ab67c68605
All checks were successful
PR Check / rust (pull_request) Successful in 21m4s
PR Check / frontend (pull_request) Successful in 2m20s
feat(budget): flip grid to income-first + add Résultat rows
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
2026-07-11 16:59:01 -04:00

94 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };
}