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 {