feat: compare report — MoM / YoY / Actual vs Budget (#73) #92

Merged
maximus merged 1 commit from issue-73-compare into main 2026-04-14 19:00:33 +00:00
Owner

Fixes #73

Rapport /reports/compare avec 3 modes accessibles via un tab bar secondaire.

Services (SQL strictement paramétrés)

  • getCompareMonthOverMonth(year, month) — expense totals par catégorie, mois cible vs mois précédent, ORDER BY ABS(delta) DESC
  • getCompareYearOverYear(year) — même logique sur 12 mois vs 12 mois
  • getBudgetVsActualData réutilisé tel quel (pas de nouveau service)

Types

  • Nouveau CategoryDelta partagé ; HighlightMover devient un alias

Hook useCompare

  • Reducer mode/year/month/rows/loading/error
  • Infère year/month depuis le to de useReportsPeriod (bookmarkable)
  • Budget mode skip fetch (délégué à CompareBudgetView)

Composants

  • CompareModeTabs — tab bar tri-état
  • ComparePeriodTable — tableau avec coloration signée du delta
  • ComparePeriodChart — diverging bar chart horizontal Recharts centré sur 0 + ChartPatternDefs
  • CompareBudgetView — wrapper autour de BudgetVsActualTable existant, charge les données pour la cible

Page

  • ReportsComparePage complète avec PeriodSelector + CompareModeTabs + ViewModeToggle (storage key reports-viewmode-compare), masqué en mode budget

i18n

  • reports.compare.modeMoM / modeYoY / modeBudget FR + EN, parité OK

Tests

  • npm run build
  • npm test 46/46 (4 nouveaux : boundary params, wrap janvier, conversion delta, YoY span)
  • cargo check
Fixes #73 Rapport `/reports/compare` avec 3 modes accessibles via un tab bar secondaire. ## Services (SQL strictement paramétrés) - `getCompareMonthOverMonth(year, month)` — expense totals par catégorie, mois cible vs mois précédent, `ORDER BY ABS(delta) DESC` - `getCompareYearOverYear(year)` — même logique sur 12 mois vs 12 mois - `getBudgetVsActualData` réutilisé tel quel (pas de nouveau service) ## Types - Nouveau `CategoryDelta` partagé ; `HighlightMover` devient un alias ## Hook `useCompare` - Reducer mode/year/month/rows/loading/error - Infère year/month depuis le `to` de `useReportsPeriod` (bookmarkable) - Budget mode skip fetch (délégué à `CompareBudgetView`) ## Composants - `CompareModeTabs` — tab bar tri-état - `ComparePeriodTable` — tableau avec coloration signée du delta - `ComparePeriodChart` — diverging bar chart horizontal Recharts centré sur 0 + `ChartPatternDefs` - `CompareBudgetView` — wrapper autour de `BudgetVsActualTable` existant, charge les données pour la cible ## Page - `ReportsComparePage` complète avec `PeriodSelector` + `CompareModeTabs` + `ViewModeToggle` (storage key `reports-viewmode-compare`), masqué en mode budget ## i18n - `reports.compare.modeMoM / modeYoY / modeBudget` FR + EN, parité OK ## Tests - `npm run build` ✅ - `npm test` ✅ 46/46 (4 nouveaux : boundary params, wrap janvier, conversion delta, YoY span) - `cargo check` ✅
maximus added 1 commit 2026-04-14 18:57:32 +00:00
feat: compare report — MoM / YoY / budget with view toggle (#73)
Some checks failed
PR Check / rust (push) Successful in 23m24s
PR Check / frontend (push) Successful in 2m17s
PR Check / frontend (pull_request) Has been cancelled
PR Check / rust (pull_request) Has been cancelled
ff350d75e7
- Services: getCompareMonthOverMonth(year, month) and getCompareYearOverYear(year)
  return CategoryDelta[] (expense-side, ABS aggregates, parameterised SQL only)
- Shared CategoryDelta type with HighlightMover now aliased to it
- Flesh out useCompare hook: reducer + fetch + automatic year/month inference
  from the shared useReportsPeriod `to` date; budget mode skips fetch and
  delegates to CompareBudgetView which wraps the existing BudgetVsActualTable
- Components: CompareModeTabs (MoM/YoY/Budget tabs), ComparePeriodTable (sortable
  table with signed delta coloring), ComparePeriodChart (diverging horizontal
  bar chart with ChartPatternDefs for SVG patterns), CompareBudgetView
  (fetches budget rows for the current target year/month)
- ReportsComparePage wires everything with PeriodSelector + ViewModeToggle
  (storage key reports-viewmode-compare); chart/table toggle is hidden in budget
  mode since the budget table has its own presentation
- i18n keys: reports.compare.modeMoM / modeYoY / modeBudget in FR + EN
- 4 new vitest cases for the compare services: parameterised boundaries,
  January wrap-around to December previous year, delta conversion with
  previous=0 fallback to null pct, year-over-year spans

Fixes #73

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Review — APPROVE

Sécurité

  • Les deux nouveaux services sont strictement paramétrés (tests dédiés vérifient l'absence de littéraux 2026' dans les SQL et la présence des placeholders $N)
  • Aucune interpolation ${year} dans les requêtes — seuls les helpers JS manipulent les strings avant de les passer en params
  • Pas de XSS dans les tableaux (category_name rendu comme enfant React)

Correctness

  • Wrap janvier → décembre année précédente validé par test
  • deltaPct = null quand previous=0 (évite division par zéro) — test dédié
  • YoY borne 12 mois × 12 mois corrects
  • Budget mode skip fetch (évite doubles-calls inutiles)
  • useCompare synchronise year/month depuis le to de useReportsPeriod → bookmarkable via URL
  • Tri ORDER BY ABS(delta) DESC → plus gros mouvements en tête

Qualité

  • npm run build
  • npm test 46/46
  • cargo check
  • Parité i18n FR/EN
  • Convention flat respectée (préfixes Compare*)
  • Signature visuelle : patterns SVG + couleurs de catégorie

Non-bloquant

  • Les noms de mois sont hardcodés FR/EN (au lieu d'Intl.DateTimeFormat) — OK pour 12 valeurs
  • CompareBudgetView charge ses données séparément (hook skip budget mode) — trade-off acceptable

Ready to merge.

## Review — APPROVE ### Sécurité ✅ - Les deux nouveaux services sont **strictement paramétrés** (tests dédiés vérifient l'absence de littéraux `2026'` dans les SQL et la présence des placeholders `$N`) - Aucune interpolation `${year}` dans les requêtes — seuls les helpers JS manipulent les strings avant de les passer en params - Pas de XSS dans les tableaux (category_name rendu comme enfant React) ### Correctness ✅ - Wrap janvier → décembre année précédente validé par test - `deltaPct = null` quand previous=0 (évite division par zéro) — test dédié - YoY borne 12 mois × 12 mois corrects - Budget mode skip fetch (évite doubles-calls inutiles) - `useCompare` synchronise year/month depuis le `to` de `useReportsPeriod` → bookmarkable via URL - Tri `ORDER BY ABS(delta) DESC` → plus gros mouvements en tête ### Qualité ✅ - `npm run build` ✅ - `npm test` ✅ 46/46 - `cargo check` ✅ - Parité i18n FR/EN ✅ - Convention flat respectée (préfixes `Compare*`) - Signature visuelle : patterns SVG + couleurs de catégorie ### Non-bloquant - Les noms de mois sont hardcodés FR/EN (au lieu d'Intl.DateTimeFormat) — OK pour 12 valeurs - `CompareBudgetView` charge ses données séparément (hook skip budget mode) — trade-off acceptable Ready to merge.
maximus merged commit b3b832650f into main 2026-04-14 19:00:33 +00:00
maximus deleted branch issue-73-compare 2026-04-14 19:00:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#92
No description provided.