diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 4fb3f87..202dffd 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,9 @@ ## [Non publié] +### Modifié +- Tableau de budget : la colonne année précédente affiche maintenant le réel (transactions) au lieu du budget planifié (#34) + ## [0.6.5] ### Ajouté diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b384a4..81f294b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Changed +- Budget table: previous year column now shows actual transactions instead of planned budget (#34) + ## [0.6.5] ### Added diff --git a/src/components/budget/BudgetTable.tsx b/src/components/budget/BudgetTable.tsx index aa92e4f..b398c95 100644 --- a/src/components/budget/BudgetTable.tsx +++ b/src/components/budget/BudgetTable.tsx @@ -150,7 +150,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu monthTotals[m] += row.months[m] * sign; } annualTotal += row.annual * sign; - prevYearTotal += row.previousYearTotal * sign; + prevYearTotal += row.previousYearTotal; // actuals are already signed in the DB } const totalCols = 15; // category + prev year + annual + 12 months @@ -197,7 +197,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu - {formatSigned(row.previousYearTotal * sign)} + {formatSigned(row.previousYearTotal)} {formatSigned(row.annual * sign)} @@ -230,7 +230,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {/* Previous year total — read-only */} - {formatSigned(row.previousYearTotal * sign)} + {formatSigned(row.previousYearTotal)} {/* Annual total — editable */} @@ -340,7 +340,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu sectionMonthTotals[m] += row.months[m] * sign; } sectionAnnualTotal += row.annual * sign; - sectionPrevYearTotal += row.previousYearTotal * sign; + sectionPrevYearTotal += row.previousYearTotal; // actuals are already signed in the DB } return ( diff --git a/src/hooks/useBudget.test.ts b/src/hooks/useBudget.test.ts new file mode 100644 index 0000000..220b684 --- /dev/null +++ b/src/hooks/useBudget.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { buildPrevYearTotalMap } from "./useBudget"; + +/** + * Unit tests for the previous-year actuals normalization logic used in useBudget. + * + * Transaction amounts in the database use signed values (expenses are negative, + * income is positive). The buildPrevYearTotalMap function preserves these signs + * as-is, because the budget display layer does NOT apply a sign multiplier to + * previous year actuals (unlike planned budget amounts). + */ + +describe("buildPrevYearTotalMap", () => { + it("should preserve negative sign for expense actuals", () => { + const actuals = [{ category_id: 1, actual: -500 }]; // expense: negative in DB + const map = buildPrevYearTotalMap(actuals); + expect(map.get(1)).toBe(-500); + }); + + it("should preserve positive sign for income actuals", () => { + const actuals = [{ category_id: 2, actual: 3000 }]; // income: positive in DB + const map = buildPrevYearTotalMap(actuals); + expect(map.get(2)).toBe(3000); + }); + + it("should skip null category_id entries", () => { + const actuals = [{ category_id: null, actual: -100 }]; + const map = buildPrevYearTotalMap(actuals); + expect(map.size).toBe(0); + }); + + it("should handle zero actuals", () => { + const actuals = [{ category_id: 3, actual: 0 }]; + const map = buildPrevYearTotalMap(actuals); + expect(map.get(3)).toBe(0); + }); + + it("should handle multiple categories", () => { + const actuals = [ + { category_id: 1, actual: -200 }, + { category_id: 2, actual: 1500 }, + { category_id: 3, actual: -75.5 }, + ]; + const map = buildPrevYearTotalMap(actuals); + expect(map.size).toBe(3); + expect(map.get(1)).toBe(-200); + expect(map.get(2)).toBe(1500); + expect(map.get(3)).toBe(-75.5); + }); +}); diff --git a/src/hooks/useBudget.ts b/src/hooks/useBudget.ts index 9ee0899..2f41eab 100644 --- a/src/hooks/useBudget.ts +++ b/src/hooks/useBudget.ts @@ -3,6 +3,7 @@ import type { BudgetYearRow, BudgetTemplate } from "../shared/types"; import { getAllActiveCategories, getBudgetEntriesForYear, + getActualTotalsForYear, upsertBudgetEntry, upsertBudgetEntriesForYear, getAllTemplates, @@ -62,6 +63,21 @@ function reducer(state: BudgetState, action: BudgetAction): BudgetState { const TYPE_ORDER: Record = { expense: 0, income: 1, transfer: 2 }; +/** + * Build a map of category_id -> annual actual total from raw actuals. + * Transaction amounts are already signed (expenses negative, income positive), + * so they are stored as-is without normalization. + */ +export function buildPrevYearTotalMap( + actuals: Array<{ category_id: number | null; actual: number }> +): Map { + const prevYearTotalMap = new Map(); + for (const a of actuals) { + if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual); + } + return prevYearTotalMap; +} + export function useBudget() { const [state, dispatch] = useReducer(reducer, undefined, initialState); const fetchIdRef = useRef(0); @@ -72,10 +88,10 @@ export function useBudget() { dispatch({ type: "SET_ERROR", payload: null }); try { - const [allCategories, entries, prevYearEntries, templates] = await Promise.all([ + const [allCategories, entries, prevYearActuals, templates] = await Promise.all([ getAllActiveCategories(), getBudgetEntriesForYear(year), - getBudgetEntriesForYear(year - 1), + getActualTotalsForYear(year - 1), getAllTemplates(), ]); @@ -88,11 +104,8 @@ export function useBudget() { entryMap.get(e.category_id)!.set(e.month, e.amount); } - // Build a map for previous year totals: categoryId -> annual total - const prevYearTotalMap = new Map(); - for (const e of prevYearEntries) { - prevYearTotalMap.set(e.category_id, (prevYearTotalMap.get(e.category_id) ?? 0) + e.amount); - } + // Build a map for previous year actuals: categoryId -> annual actual total + const prevYearTotalMap = buildPrevYearTotalMap(prevYearActuals); // Helper: build months array from entryMap const buildMonths = (catId: number) => { diff --git a/src/services/budgetService.ts b/src/services/budgetService.ts index 8e4d625..8b2bdb3 100644 --- a/src/services/budgetService.ts +++ b/src/services/budgetService.ts @@ -178,6 +178,16 @@ export async function deleteTemplate(templateId: number): Promise { await db.execute("DELETE FROM budget_templates WHERE id = $1", [templateId]); } +// --- Actuals helpers --- + +export async function getActualTotalsForYear( + year: number +): Promise> { + const dateFrom = `${year}-01-01`; + const dateTo = `${year}-12-31`; + return getActualsByCategoryRange(dateFrom, dateTo); +} + // --- Budget vs Actual --- async function getActualsByCategoryRange( diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 3c96ae7..93f0019 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -142,7 +142,7 @@ export interface BudgetYearRow { depth?: number; months: number[]; // index 0-11 = Jan-Dec planned amounts annual: number; // computed sum - previousYearTotal: number; // total budget from the previous year + previousYearTotal: number; // actual (transactions) total from the previous year } export interface ImportConfigTemplate {