From c5a3e0f696e7d42d453f2dd2f28428626e0e9d22 Mon Sep 17 00:00:00 2001 From: medic-bot Date: Mon, 9 Mar 2026 14:05:47 -0400 Subject: [PATCH 1/4] feat: sticky category column, month dropdown selector, default to last completed month (#29) - Add sticky left-0 positioning to all category cells in BudgetVsActualTable - Replace MonthNavigator arrows with inline title + dropdown month selector - Default budget month to previous completed month instead of current - Add i18n keys for new title prefix (FR/EN) Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.fr.md | 3 ++ CHANGELOG.md | 3 ++ .../reports/BudgetVsActualTable.tsx | 14 ++++-- src/hooks/useReports.ts | 10 ++-- src/i18n/locales/en.json | 3 +- src/i18n/locales/fr.json | 3 +- src/pages/ReportsPage.tsx | 46 ++++++++++++++----- 7 files changed, 61 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index ba58876..dbd0204 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -12,6 +12,9 @@ ### Modifié - Tableau de bord : le graphique circulaire prend 1/3 de la largeur au lieu de 1/2, donnant plus d'espace au tableau budget (#23) - Tableau de bord : les étiquettes du graphique circulaire s'affichent uniquement au survol via le tooltip (#23) +- Budget vs Réel : la colonne des catégories reste désormais fixe lors du défilement horizontal (#29) +- Budget vs Réel : titre changé pour « Budget vs Réel pour le mois de [mois] » avec un menu déroulant pour sélectionner le mois (#29) +- Budget vs Réel : le mois par défaut est maintenant le dernier mois complété au lieu du mois courant (#29) ## [0.6.3] diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0c18a..d331fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ ### Changed - Dashboard: pie chart takes 1/3 width instead of 1/2, giving more space to the budget table (#23) - Dashboard: pie chart labels now shown only on hover via tooltip instead of permanent legend (#23) +- Budget vs Actual: category column now stays fixed when scrolling horizontally (#29) +- Budget vs Actual: title changed to "Budget vs Réel pour le mois de [month]" with a dropdown month selector (#29) +- Budget vs Actual: default month is now the last completed month instead of current month (#29) ## [0.6.3] diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index a7ef69f..716f7fc 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -103,7 +103,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) - - @@ -177,7 +177,11 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) isIntermediateParent ? "bg-[var(--muted)]/15 font-medium" : "" }`} > - + @@ -237,7 +241,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) })} {/* Grand totals */} - + diff --git a/src/hooks/useReports.ts b/src/hooks/useReports.ts index 16ae339..defbd71 100644 --- a/src/hooks/useReports.ts +++ b/src/hooks/useReports.ts @@ -59,8 +59,8 @@ const initialState: ReportsState = { monthlyTrends: [], categorySpending: [], categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} }, - budgetYear: now.getFullYear(), - budgetMonth: now.getMonth() + 1, + budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(), + budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(), budgetVsActual: [], pivotConfig: { rows: [], columns: [], filters: {}, values: [] }, pivotResult: { rows: [], columnValues: [], dimensionLabels: {} }, @@ -237,6 +237,10 @@ export function useReports() { dispatch({ type: "SET_BUDGET_MONTH", payload: { year: newYear, month: newMonth } }); }, [state.budgetYear, state.budgetMonth]); + const setBudgetMonth = useCallback((year: number, month: number) => { + dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } }); + }, []); + const setCustomDates = useCallback((dateFrom: string, dateTo: string) => { dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } }); }, []); @@ -249,5 +253,5 @@ export function useReports() { dispatch({ type: "SET_SOURCE_ID", payload: id }); }, []); - return { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setPivotConfig, setSourceId }; + return { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setBudgetMonth, setPivotConfig, setSourceId }; } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b1da85f..7de1e70 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -377,7 +377,8 @@ "ytd": "Year-to-Date", "dollarVar": "$ Var", "pctVar": "% Var", - "noData": "No budget or transaction data for this period." + "noData": "No budget or transaction data for this period.", + "titlePrefix": "Budget vs Actual for" }, "dynamic": "Dynamic Report", "export": "Export", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 28abfd7..0f32f2c 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -377,7 +377,8 @@ "ytd": "Cumul annuel", "dollarVar": "$ \u00c9cart", "pctVar": "% \u00c9cart", - "noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode." + "noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode.", + "titlePrefix": "Budget vs Réel pour le mois de" }, "dynamic": "Rapport dynamique", "export": "Exporter", diff --git a/src/pages/ReportsPage.tsx b/src/pages/ReportsPage.tsx index 906a193..b654fd5 100644 --- a/src/pages/ReportsPage.tsx +++ b/src/pages/ReportsPage.tsx @@ -6,7 +6,6 @@ import { PageHelp } from "../components/shared/PageHelp"; import type { ReportTab, CategoryBreakdownItem, DashboardPeriod, ImportSource } from "../shared/types"; import { getAllSources } from "../services/importSourceService"; import PeriodSelector from "../components/dashboard/PeriodSelector"; -import MonthNavigator from "../components/budget/MonthNavigator"; import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart"; import MonthlyTrendsTable from "../components/reports/MonthlyTrendsTable"; import CategoryBarChart from "../components/reports/CategoryBarChart"; @@ -48,8 +47,8 @@ function computeDateRange( } export default function ReportsPage() { - const { t } = useTranslation(); - const { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setPivotConfig, setSourceId } = useReports(); + const { t, i18n } = useTranslation(); + const { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId } = useReports(); const [sources, setSources] = useState([]); useEffect(() => { @@ -100,16 +99,41 @@ export default function ReportsPage() {
-

{t("reports.title")}

+ {state.tab === "budgetVsActual" ? ( +

+ {t("reports.bva.titlePrefix")} + +

+ ) : ( +

{t("reports.title")}

+ )}
- {state.tab === "budgetVsActual" ? ( - - ) : ( + {state.tab !== "budgetVsActual" && ( Date: Mon, 9 Mar 2026 20:48:08 -0400 Subject: [PATCH 2/4] fix: address reviewer feedback (#29) - Replace semi-transparent backgrounds on sticky columns with opaque color-mix equivalents so scrolled content is fully hidden - Add opaque background to section header sticky td - Extract IIFE month options in ReportsPage into a useMemo --- .../reports/BudgetVsActualTable.tsx | 20 +++++------ src/pages/ReportsPage.tsx | 34 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index 716f7fc..d449bcb 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -158,8 +158,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null; return ( -
- + @@ -173,13 +173,13 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) ); })} - - + + @@ -240,8 +240,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) ); })} {/* Grand totals */} - - + + diff --git a/src/pages/ReportsPage.tsx b/src/pages/ReportsPage.tsx index b654fd5..0102b19 100644 --- a/src/pages/ReportsPage.tsx +++ b/src/pages/ReportsPage.tsx @@ -92,6 +92,19 @@ export default function ReportsPage() { return []; }, [state.tab, state.categorySpending, state.categoryOverTime]); + const monthOptions = useMemo(() => { + const now = new Date(); + const currentMonth = now.getMonth(); + const currentYear = now.getFullYear(); + return Array.from({ length: 24 }, (_, i) => { + const d = new Date(currentYear, currentMonth - i, 1); + const y = d.getFullYear(); + const m = d.getMonth() + 1; + const label = new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(d); + return { key: `${y}-${m}`, value: `${y}-${m}`, label: label.charAt(0).toUpperCase() + label.slice(1) }; + }); + }, [i18n.language]); + const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0; const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1); @@ -110,22 +123,11 @@ export default function ReportsPage() { }} className="text-2xl font-bold bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-1 cursor-pointer hover:bg-[var(--muted)] transition-colors" > - {(() => { - const now = new Date(); - const currentMonth = now.getMonth(); // 0-based - const currentYear = now.getFullYear(); - return Array.from({ length: 24 }, (_, i) => { - const d = new Date(currentYear, currentMonth - i, 1); - const y = d.getFullYear(); - const m = d.getMonth() + 1; - const label = new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(d); - return ( - - ); - }); - })()} + {monthOptions.map((opt) => ( + + ))} ) : ( -- 2.45.2 From 8971e443d86f577c6dcdbd85285367d6f7888254 Mon Sep 17 00:00:00 2001 From: medic-bot Date: Mon, 9 Mar 2026 21:03:42 -0400 Subject: [PATCH 3/4] fix: address reviewer feedback (#29) - Remove dead code: navigateBudgetMonth function and its export from useReports.ts (replaced by setBudgetMonth, no longer imported anywhere) Co-Authored-By: Claude Opus 4.6 --- src/hooks/useReports.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/hooks/useReports.ts b/src/hooks/useReports.ts index defbd71..0cbfc5a 100644 --- a/src/hooks/useReports.ts +++ b/src/hooks/useReports.ts @@ -224,19 +224,6 @@ export function useReports() { dispatch({ type: "SET_PERIOD", payload: period }); }, []); - const navigateBudgetMonth = useCallback((delta: -1 | 1) => { - let newMonth = state.budgetMonth + delta; - let newYear = state.budgetYear; - if (newMonth < 1) { - newMonth = 12; - newYear -= 1; - } else if (newMonth > 12) { - newMonth = 1; - newYear += 1; - } - dispatch({ type: "SET_BUDGET_MONTH", payload: { year: newYear, month: newMonth } }); - }, [state.budgetYear, state.budgetMonth]); - const setBudgetMonth = useCallback((year: number, month: number) => { dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } }); }, []); @@ -253,5 +240,5 @@ export function useReports() { dispatch({ type: "SET_SOURCE_ID", payload: id }); }, []); - return { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setBudgetMonth, setPivotConfig, setSourceId }; + return { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId }; } -- 2.45.2 From 64b7d8d11b70daac0c728524272025227c2e7f88 Mon Sep 17 00:00:00 2001 From: medic-bot Date: Mon, 9 Mar 2026 21:19:06 -0400 Subject: [PATCH 4/4] fix: remove duplicated px-3 class and improve readability (#29) Clean up the sticky category cell className: remove the duplicated px-3 for top-level parents and break the long ternary into readable multi-line format. Co-Authored-By: Claude Opus 4.6 --- src/components/reports/BudgetVsActualTable.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index d449bcb..63218a9 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -178,9 +178,11 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) }`} >
+ {t("budget.category")} @@ -159,7 +159,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) return (
+ {section.label}
+ - {t(typeTotalKeys[section.type])}{t(typeTotalKeys[section.type])} {cadFormatter(sectionTotals.monthActual)}
{t("common.total")}{t("common.total")} {cadFormatter(totals.monthActual)}
+
{section.label}
@@ -213,8 +213,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
{t(typeTotalKeys[section.type])}
{t(typeTotalKeys[section.type])} {cadFormatter(sectionTotals.monthActual)}
{t("common.total")}
{t("common.total")} {cadFormatter(totals.monthActual)}