feat: new Cartes dashboard report — KPI cards, sparklines, top movers (#97) #99

Merged
maximus merged 1 commit from issue-97-cartes-page into main 2026-04-15 23:53:38 +00:00
Owner

Summary

New /reports/cartes sub-report — a dashboard-style snapshot of the reference month. Composes four KPI cards, an overlay chart, top movers, budget adherence, and a seasonality card into a single screen.

Widgets

  • 4 KPI cards — Income, Expenses, Net balance, Savings rate. Each card shows the reference-month value, the MoM and YoY deltas simultaneously (with absolute + percentage), and a 13-month sparkline with the reference month highlighted by a filled dot. Delta colors flip based on whether "up" is good or bad for that metric.
  • Income vs Expenses overlay chart — 12-month ComposedChart with grouped bars for income/expenses and an overlaid line for the net balance. Recharts + Tailwind tokens, dark-mode ready.
  • Top movers — Top 5 categories that increased the most MoM and top 5 that decreased the most, rendered as two compact lists. Each row is clickable and navigates to /reports/category?cat=<id>.
  • Budget adherence cardN/M categories on target, a breakdown sentence, and the 3 worst overruns with signed-percentage and a progress bar.
  • Seasonality card — Reference month amount vs. the same calendar month across the two previous years, with an average and a deviation indicator that turns red/green once the spread crosses ±5%.

Service

All widgets are hydrated from a single getCartesSnapshot(referenceYear, referenceMonth) call that runs four queries in parallel (Promise.all):

  1. 25-month expense/income flow (one query, covers the 13-month sparkline, the 12-month overlay chart, and the MoM/YoY lookups — no extra round trips)
  2. getCompareMonthOverMonth for top movers
  3. getBudgetVsActualData for budget adherence
  4. Seasonality same-month aggregation across the two previous years

Missing months are filled with zeros in the sparklines so the chart stays continuous, but the MoM/YoY deltas remain null when the comparison month has no data — the UI can distinguish "no data" from "zero spend".

Hook & page

  • useCartes() is a standard reducer-based hook with { year, month, snapshot, isLoading, error } and a setReferencePeriod(year, month) setter. It stays in sync with useReportsPeriod so the URL PeriodSelector and the in-page dropdown can both move the reference month.
  • ReportsCartesPage lays out the sections responsively (sm:grid-cols-2 xl:grid-cols-4 for KPI cards, lg:grid-cols-2 for movers / budget / seasonality).

i18n

All copy in FR + EN under reports.cartes.* plus a new hub entry reports.hub.cartes / cartesDescription.

⚠️ Note on CompareReferenceMonthPicker.tsx

This file is a temporary duplicate — the canonical copy lives on the issue-96-compare-refactor branch (PR #98). Both copies are byte-identical. Once either PR merges first, the other will see the file already present with the same content and no conflict. I kept the local copy so this branch builds cleanly on main without depending on #98. Feel free to drop it during review of the second-to-merge PR if you prefer.

Test plan

  • npm run build — green (tsc + vite), 2541 modules
  • npm test — 78/78 vitest cases passing (16 new: 13 in reportService.cartes.test.ts + 3 in useCartes.test.ts)
    • Empty-data snapshot (all KPIs zero)
    • MoM/YoY delta computation from a monthly flow stream
    • January reference month wraps MoM to December, YoY to null if no prior-year data
    • Savings rate stays at 0 when income is zero (no division by zero)
    • Sparkline fills gaps with zero but MoM/YoY stay null when the comparison month is missing
    • Seasonality with historical data → average + deviation
    • Seasonality without history → deviation stays null
    • Top movers split by sign and capped at 5 each
    • Pure helpers (shiftMonth + defaultCartesReferencePeriod) covered across year boundaries
  • Manual QA in the desktop app:
    • Reference month dropdown defaults to the previous month
    • Switching the reference month re-hydrates all six widgets
    • Sparklines highlight the reference month with a filled dot
    • Top mover list links navigate to the correct category zoom
    • Budget adherence card renders empty state gracefully when no budgets exist
    • Seasonality card hides deviation when history is missing
    • Dark mode rendering across all cards
    • FR and EN translations both render correctly

Dependencies

Can be merged before or after #98 (refactor: compare report) — the only shared touchpoints are src/services/reportService.ts and src/i18n/locales/*.json, with no semantic conflicts. See the note above about CompareReferenceMonthPicker.tsx.

Closes #97

## Summary New `/reports/cartes` sub-report — a dashboard-style snapshot of the reference month. Composes four KPI cards, an overlay chart, top movers, budget adherence, and a seasonality card into a single screen. **Widgets** - **4 KPI cards** — Income, Expenses, Net balance, Savings rate. Each card shows the reference-month value, the **MoM and YoY deltas simultaneously** (with absolute + percentage), and a 13-month sparkline with the reference month highlighted by a filled dot. Delta colors flip based on whether "up" is good or bad for that metric. - **Income vs Expenses overlay chart** — 12-month `ComposedChart` with grouped bars for income/expenses and an overlaid line for the net balance. Recharts + Tailwind tokens, dark-mode ready. - **Top movers** — Top 5 categories that increased the most MoM and top 5 that decreased the most, rendered as two compact lists. Each row is clickable and navigates to `/reports/category?cat=<id>`. - **Budget adherence card** — `N/M categories on target`, a breakdown sentence, and the 3 worst overruns with signed-percentage and a progress bar. - **Seasonality card** — Reference month amount vs. the same calendar month across the two previous years, with an average and a deviation indicator that turns red/green once the spread crosses ±5%. **Service** All widgets are hydrated from a single `getCartesSnapshot(referenceYear, referenceMonth)` call that runs four queries in parallel (`Promise.all`): 1. 25-month expense/income flow (one query, covers the 13-month sparkline, the 12-month overlay chart, and the MoM/YoY lookups — no extra round trips) 2. `getCompareMonthOverMonth` for top movers 3. `getBudgetVsActualData` for budget adherence 4. Seasonality same-month aggregation across the two previous years Missing months are filled with zeros in the sparklines so the chart stays continuous, but the MoM/YoY deltas remain `null` when the comparison month has no data — the UI can distinguish "no data" from "zero spend". **Hook & page** - `useCartes()` is a standard reducer-based hook with `{ year, month, snapshot, isLoading, error }` and a `setReferencePeriod(year, month)` setter. It stays in sync with `useReportsPeriod` so the URL PeriodSelector and the in-page dropdown can both move the reference month. - `ReportsCartesPage` lays out the sections responsively (`sm:grid-cols-2 xl:grid-cols-4` for KPI cards, `lg:grid-cols-2` for movers / budget / seasonality). **i18n** All copy in FR + EN under `reports.cartes.*` plus a new hub entry `reports.hub.cartes` / `cartesDescription`. **⚠️ Note on `CompareReferenceMonthPicker.tsx`** This file is a **temporary duplicate** — the canonical copy lives on the `issue-96-compare-refactor` branch (PR #98). Both copies are byte-identical. Once either PR merges first, the other will see the file already present with the same content and no conflict. I kept the local copy so this branch builds cleanly on `main` without depending on #98. Feel free to drop it during review of the second-to-merge PR if you prefer. ## Test plan - [x] `npm run build` — green (tsc + vite), 2541 modules - [x] `npm test` — 78/78 vitest cases passing (16 new: 13 in `reportService.cartes.test.ts` + 3 in `useCartes.test.ts`) - Empty-data snapshot (all KPIs zero) - MoM/YoY delta computation from a monthly flow stream - January reference month wraps MoM to December, YoY to null if no prior-year data - Savings rate stays at 0 when income is zero (no division by zero) - Sparkline fills gaps with zero but MoM/YoY stay null when the comparison month is missing - Seasonality with historical data → average + deviation - Seasonality without history → deviation stays null - Top movers split by sign and capped at 5 each - Pure helpers (`shiftMonth` + `defaultCartesReferencePeriod`) covered across year boundaries - [ ] Manual QA in the desktop app: - [ ] Reference month dropdown defaults to the previous month - [ ] Switching the reference month re-hydrates all six widgets - [ ] Sparklines highlight the reference month with a filled dot - [ ] Top mover list links navigate to the correct category zoom - [ ] Budget adherence card renders empty state gracefully when no budgets exist - [ ] Seasonality card hides deviation when history is missing - [ ] Dark mode rendering across all cards - [ ] FR and EN translations both render correctly ## Dependencies Can be merged before or after #98 (`refactor: compare report`) — the only shared touchpoints are `src/services/reportService.ts` and `src/i18n/locales/*.json`, with no semantic conflicts. See the note above about `CompareReferenceMonthPicker.tsx`. Closes #97
maximus added 1 commit 2026-04-15 22:22:41 +00:00
feat(reports/cartes): new KPI dashboard sub-report with sparklines, top movers, budget adherence and seasonality (#97)
All checks were successful
PR Check / rust (push) Successful in 23m50s
PR Check / frontend (push) Successful in 2m21s
PR Check / rust (pull_request) Successful in 23m54s
PR Check / frontend (pull_request) Successful in 2m19s
31765e6d17
New /reports/cartes page surfaces a dashboard-style snapshot of the
reference month:

- 4 KPI cards (income / expenses / net / savings rate) showing MoM and
  YoY deltas simultaneously, each with a 13-month sparkline highlighting
  the reference month
- 12-month income vs expenses overlay chart (bars + net balance line)
- Top 5 category increases + top 5 decreases MoM, clickable through to
  the category zoom report
- Budget adherence card: on-target count + 3 worst overruns with
  progress bars
- Seasonality card: reference month vs same calendar month averaged
  over the two previous years, with deviation indicator

All data is fetched in a single getCartesSnapshot() service call that
runs four queries concurrently (25-month flow, MoM category deltas,
budget-vs-actual, seasonality). Missing months are filled with zeroes
in the sparklines but preserved as null in the MoM/YoY deltas so the UI
can distinguish "no data" from "zero spend".

- Exported pure helpers: shiftMonth, defaultCartesReferencePeriod
- 13 vitest cases covering zero data, MoM/YoY computation, January
  wrap-around, missing-month handling, division by zero for the
  savings rate, seasonality with and without history, top mover sign
  splitting and 5-cap

Note: src/components/reports/CompareReferenceMonthPicker.tsx is a
temporary duplicate — the canonical copy lives on the issue-96 branch
(refactor: compare report). Once both branches merge the content is
identical and git will dedupe. Keeping the local copy here means the
Cartes branch builds cleanly on main without depending on #96.

Closes #97

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
maximus force-pushed issue-97-cartes-page from 31765e6d17 to 4c58b8bab8 2026-04-15 23:45:46 +00:00 Compare
maximus merged commit 89b69f325e into main 2026-04-15 23:53:38 +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#99
No description provided.