diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 60e63aa..7066584 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,9 @@ ## [Non publié] +### Ajouté +- Tableau de budget : colonne du total de l'année précédente affichée comme première colonne de données pour servir de référence (#16) + ## [0.6.3] ### Ajouté diff --git a/CHANGELOG.md b/CHANGELOG.md index 491f0f9..edd44d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Added +- Budget table: previous year total column displayed as first data column for baseline reference (#16) + ## [0.6.3] ### Added diff --git a/src/components/budget/BudgetTable.tsx b/src/components/budget/BudgetTable.tsx index 09651b6..c7666c3 100644 --- a/src/components/budget/BudgetTable.tsx +++ b/src/components/budget/BudgetTable.tsx @@ -193,6 +193,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu // Column totals with sign convention (only count leaf rows to avoid double-counting parents) const monthTotals: number[] = Array(12).fill(0); let annualTotal = 0; + let prevYearTotal = 0; for (const row of rows) { if (row.is_parent) continue; // skip parent subtotals to avoid double-counting const sign = signFor(row.category_type); @@ -200,9 +201,10 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu monthTotals[m] += row.months[m] * sign; } annualTotal += row.annual * sign; + prevYearTotal += row.previousYearTotal * sign; } - const totalCols = 14; // category + annual + 12 months + const totalCols = 15; // category + prev year + annual + 12 months if (rows.length === 0) { return ( @@ -243,6 +245,9 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {row.category_name} + + {formatSigned(row.previousYearTotal * sign)} + {formatSigned(row.annual * sign)} @@ -271,6 +276,12 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {row.category_name} + {/* Previous year total — read-only */} + + + {formatSigned(row.previousYearTotal * sign)} + + {/* Annual total — editable */} {editingAnnual?.categoryId === row.category_id ? ( @@ -351,6 +362,9 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {t("budget.category")} + + {t("budget.previousYear")} + {t("budget.annual")} @@ -369,11 +383,13 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu const leaves = group.filter((r) => !r.is_parent); const sectionMonthTotals: number[] = Array(12).fill(0); let sectionAnnualTotal = 0; + let sectionPrevYearTotal = 0; for (const row of leaves) { for (let m = 0; m < 12; m++) { sectionMonthTotals[m] += row.months[m] * sign; } sectionAnnualTotal += row.annual * sign; + sectionPrevYearTotal += row.previousYearTotal * sign; } return ( @@ -390,6 +406,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {t(typeTotalKeys[type])} + {formatSigned(sectionPrevYearTotal)} {formatSigned(sectionAnnualTotal)} {sectionMonthTotals.map((total, mIdx) => ( @@ -403,6 +420,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {/* Totals row */} {t("common.total")} + {formatSigned(prevYearTotal)} {formatSigned(annualTotal)} {monthTotals.map((total, mIdx) => ( diff --git a/src/hooks/useBudget.ts b/src/hooks/useBudget.ts index d78ed17..9ee0899 100644 --- a/src/hooks/useBudget.ts +++ b/src/hooks/useBudget.ts @@ -72,9 +72,10 @@ export function useBudget() { dispatch({ type: "SET_ERROR", payload: null }); try { - const [allCategories, entries, templates] = await Promise.all([ + const [allCategories, entries, prevYearEntries, templates] = await Promise.all([ getAllActiveCategories(), getBudgetEntriesForYear(year), + getBudgetEntriesForYear(year - 1), getAllTemplates(), ]); @@ -87,6 +88,12 @@ 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); + } + // Helper: build months array from entryMap const buildMonths = (catId: number) => { const monthMap = entryMap.get(catId); @@ -97,7 +104,8 @@ export function useBudget() { months.push(val); annual += val; } - return { months, annual }; + const previousYearTotal = prevYearTotalMap.get(catId) ?? 0; + return { months, annual, previousYearTotal }; }; // Index categories by id and group children by parent_id @@ -117,7 +125,7 @@ export function useBudget() { const grandchildren = (childrenByParent.get(cat.id) || []).filter((c) => c.is_inputable); if (grandchildren.length === 0 && cat.is_inputable) { // Leaf at depth 2 - const { months, annual } = buildMonths(cat.id); + const { months, annual, previousYearTotal } = buildMonths(cat.id); return [{ category_id: cat.id, category_name: cat.name, @@ -128,6 +136,7 @@ export function useBudget() { depth: 2, months, annual, + previousYearTotal, }]; } if (grandchildren.length === 0 && !cat.is_inputable) { @@ -138,7 +147,7 @@ export function useBudget() { const gcRows: BudgetYearRow[] = []; if (cat.is_inputable) { - const { months, annual } = buildMonths(cat.id); + const { months, annual, previousYearTotal } = buildMonths(cat.id); gcRows.push({ category_id: cat.id, category_name: `${cat.name} (direct)`, @@ -149,10 +158,11 @@ export function useBudget() { depth: 2, months, annual, + previousYearTotal, }); } for (const gc of grandchildren) { - const { months, annual } = buildMonths(gc.id); + const { months, annual, previousYearTotal } = buildMonths(gc.id); gcRows.push({ category_id: gc.id, category_name: gc.name, @@ -163,6 +173,7 @@ export function useBudget() { depth: 2, months, annual, + previousYearTotal, }); } if (gcRows.length === 0) return []; @@ -170,9 +181,11 @@ export function useBudget() { // Build intermediate subtotal const subMonths = Array(12).fill(0) as number[]; let subAnnual = 0; + let subPrevYear = 0; for (const cr of gcRows) { for (let m = 0; m < 12; m++) subMonths[m] += cr.months[m]; subAnnual += cr.annual; + subPrevYear += cr.previousYearTotal; } const subtotal: BudgetYearRow = { category_id: cat.id, @@ -184,6 +197,7 @@ export function useBudget() { depth: 1, months: subMonths, annual: subAnnual, + previousYearTotal: subPrevYear, }; gcRows.sort((a, b) => { if (a.category_id === cat.id) return -1; @@ -203,7 +217,7 @@ export function useBudget() { if (inputableChildren.length === 0 && intermediateParents.length === 0 && cat.is_inputable) { // Standalone leaf (no children) — regular editable row - const { months, annual } = buildMonths(cat.id); + const { months, annual, previousYearTotal } = buildMonths(cat.id); rows.push({ category_id: cat.id, category_name: cat.name, @@ -214,13 +228,14 @@ export function useBudget() { depth: 0, months, annual, + previousYearTotal, }); } else if (inputableChildren.length > 0 || intermediateParents.length > 0) { const allChildRows: BudgetYearRow[] = []; // If parent is also inputable, create a "(direct)" fake-child row if (cat.is_inputable) { - const { months, annual } = buildMonths(cat.id); + const { months, annual, previousYearTotal } = buildMonths(cat.id); allChildRows.push({ category_id: cat.id, category_name: `${cat.name} (direct)`, @@ -231,6 +246,7 @@ export function useBudget() { depth: 1, months, annual, + previousYearTotal, }); } @@ -238,7 +254,7 @@ export function useBudget() { const grandchildren = childrenByParent.get(child.id) || []; if (grandchildren.length === 0) { // Simple leaf at depth 1 - const { months, annual } = buildMonths(child.id); + const { months, annual, previousYearTotal } = buildMonths(child.id); allChildRows.push({ category_id: child.id, category_name: child.name, @@ -249,6 +265,7 @@ export function useBudget() { depth: 1, months, annual, + previousYearTotal, }); } else { // Intermediate parent at depth 1 with grandchildren @@ -267,9 +284,11 @@ export function useBudget() { const leafRows = allChildRows.filter((r) => !r.is_parent); const parentMonths = Array(12).fill(0) as number[]; let parentAnnual = 0; + let parentPrevYear = 0; for (const cr of leafRows) { for (let m = 0; m < 12; m++) parentMonths[m] += cr.months[m]; parentAnnual += cr.annual; + parentPrevYear += cr.previousYearTotal; } rows.push({ @@ -282,6 +301,7 @@ export function useBudget() { depth: 0, months: parentMonths, annual: parentAnnual, + previousYearTotal: parentPrevYear, }); // Sort children alphabetically, but keep "(direct)" first diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 73e5a3c..b1da85f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -319,6 +319,7 @@ "actual": "Actual", "difference": "Difference", "annual": "Annual", + "previousYear": "Prev. Year", "splitEvenly": "Split evenly across 12 months", "annualMismatch": "Annual total does not match the sum of monthly amounts", "clickToEdit": "Click to edit", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index a5b5f13..28abfd7 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -319,6 +319,7 @@ "actual": "Réel", "difference": "Écart", "annual": "Annuel", + "previousYear": "Année préc.", "splitEvenly": "Répartir également sur 12 mois", "annualMismatch": "Le total annuel ne correspond pas à la somme des montants mensuels", "clickToEdit": "Cliquer pour modifier", diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index bff52b3..f32bef5 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -142,6 +142,7 @@ export interface BudgetYearRow { depth?: 0 | 1 | 2; months: number[]; // index 0-11 = Jan-Dec planned amounts annual: number; // computed sum + previousYearTotal: number; // total budget from the previous year } export interface ImportConfigTemplate {