feat(dashboard): converge / on the Cartes report model #287
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#287
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-279-dashboard-cartes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Converges the home Dashboard (
/) on the/reports/cartespresentational model, per the epic #260 "rapports uniformes" vision.KpiCards (income/expenses/net/savings rate), each showing MoM/YoY deltas, sourced fromgetCartesSnapshotagainst a Dashboard-owned reference month (defaults to the last complete month viadefaultReferencePeriod, same default as Compare/Cartes). No mode toggle (month/YTD) exposed yet — defaults to"month"(see Decisions below).TopMoversList(up/down) andBudgetAdherenceCardstraight from Cartes, same data.IncomeExpenseOverlayChart(income vs expenses, 12 months) from the Cartes snapshot. The existing "expenses over time" chart now explicitly passestypeFilter: "expense"togetCategoryOverTime— previously it silently aggregated every category type despite its heading, so a revenue category could show up in what read as an expense-only trend.CategoryBarChart.tsx, a fully-formed component that had been orphaned since an earlierReportsCategoryPagerewrite — same props contract as the oldCategoryPieChart, so this is a drop-in swap with zero new component code.getSnapshotTotalsByDate), reusing the exactbalance.overview.latestTotal/asOfcopy already shown on/balance— same words, so the label can't be confused with the account (import-source) filter below it. Hidden (never a misleading "$0") until at least one balance account has a recorded snapshot; the visibility decision reusesderiveLandingState(balanceLanding.ts) rather than re-inferring emptiness from nulls, per the review caveat. Fetched independently on mount — not scoped by the account filter (balance_accountsis a disjoint concept fromimport_sources).<FilterPanel>(temporal slot = existingPeriodSelector, plus the import-source multi-select already used on Trends/Compare/Budget). The filter is Dashboard-local state (accountIdsinuseDashboard's own reducer), not routed throughuseReportsPeriod— the Dashboard keeps its own two temporal axes (a flexible period for the bar chart/trend, and the separate reference month for the Cartes-sourced section), matching the M1 review note that I4 deliberately left the Dashboard untouched.New pure helper
deriveNetWorthTileindashboardService.ts(tested, 5 new cases) shapes the tile's visibility/value from the same two signalsBalancePageuses.Decisions (mid-tier ambiguity, noted per autopilot policy)
CartesPeriodModeToggle; the issue's acceptance criteria only asked for "KPIs with MoM/YoY deltas", not the toggle itself. Defaulted tomode: "month"to keep the page's control surface tighter. Rejected alternative: reuseCartesPeriodModeToggletoo — deferred as a cheap follow-up if wanted, sincegetCartesSnapshotalready supports it.dashboard.balanceandreports.bva.titlePrefixi18n keys removed (FR+EN) — both became orphaned by this change specifically (verified via grep: no other consumer); their sibling keys in the same namespaces remain in use elsewhere and were left untouched.getDashboardSummaryleft in place, unused by the page — no longer called fromuseDashboard, but still exported/untested-elsewhere; removing it outright felt like unrelated scope creep for a same-file, still-public function.CategoryPieChart.tsxleft in place, now orphaned — mirrors howCategoryBarChart.tsxitself sat unused for a long time before this PR resurrected it; kept in case a future "chart type toggle" wants it back.Reviewer caveats
fetchMonthlyFlows/fetchSeasonalityseries remain un-filtered byaccountIds(only its two sub-reports — top movers, budget adherence — respect the filter, per the pre-existinggetCartesSnapshotdoc comment from #273/#276). Dashboard reuses that same snapshot as-is; the KPI cards'current/deltas and the overlay chart therefore do not reflect the account filter, only top movers and budget adherence do. Flagged as pre-existing, out of scope for this issue (the M1 review note explicitly scoped that gap to a future issue).flex flex-wrapcontainer (not the fixed 4-column grid Cartes uses for its own KPI row) specifically so the layout degrades cleanly to 4 items when the tile is hidden (no empty grid cell).Generated autonomously by /autopilot run of 2026-07-11
Test plan
npm run build(tsc + vite) greennpm test— 796 passed (baseline 791 + 5 newderiveNetWorthTilecases), 0 regressionscargo checkgreen (no Rust touched)/pr-review — PR #287 (issue #279) : converge
/sur le modèle CartesVerdict révisé : REQUEST_CHANGES
La convergence est bien faite : contrats résolus, types concordants, i18n complète FR/EN,
deriveNetWorthTilepur et testé (5 cas), tuile valeur nette correctement découplée du filtre, retraits de clés orphelines vérifiés,CategoryBarCharten vrai drop-in. Le problème est ailleurs.🔴 Bloquant — le filtre de comptes ne s'applique pas aux KPIs ni au graphe overlay, et cette PR est la première page à exposer le filtre
Fait (tracé dans le code de la branche) :
getCartesSnapshot(src/services/reportService.ts:1250) ne transmetaccountIdsqu'à ses deux sous-rapports :fetchMonthlyFlows(reportService.ts:1186) etfetchSeasonality(reportService.ts:1209) ne prennent aucun paramètreaccountIds— leur SQL est unFROM transactions WHERE date …non filtré parsource_id. Or les 4 KPI cards (revenus / dépenses / net / taux d'épargne) sont calculés depuisflowByMonth, lui-même construit à partir deflowRows=fetchMonthlyFlows. Le grapheIncomeExpenseOverlayChartconsommeflow12Months, dérivé du mêmeflowByMonth. Ces widgets ignorent doncaccountIds.En parallèle, sur la même page, ces widgets respectent le filtre :
getExpensesByCategory(dateFrom, dateTo, accountIds)(useDashboard.ts)getCategoryOverTime(…, accountIds, "expense")Scénario d'échec concret : l'utilisateur sélectionne un seul compte dans le
<FilterPanel>que cette PR ajoute au Dashboard (DashboardPage.tsx,accountIds={state.accountIds}). La KPI « Dépenses » reste le total tous comptes, alors que les barres de dépenses par catégorie juste en dessous sont filtrées sur ce compte. Les deux ne réconcilient pas : la somme des barres ≠ la KPI Dépenses. Dans une app de finances personnelles, deux chiffres contradictoires sous un même filtre que l'utilisateur vient de poser est un défaut de correction, pas une coquetterie cosmétique.Pourquoi c'est cette PR (et pas un héritage à ignorer) : la page
/reports/cartesn'a jamais exposé ce filtre —useCartes.ts:81appellegetCartesSnapshot(year, month, mode)sansaccountIds, etReportsCartesPage.tsxne rend aucunFilterPanel. Le gap était donc dormant en prod (tout non filtré = cohérent). Cette PR est la première et seule page à la fois à exposer le multi-select de comptes et à passeraccountIdsàgetCartesSnapshot. C'est elle qui rend l'incohérence visible — le planning M2 avait justement flaggé ce risque (I7) à vérifier en review, précisément pour cette raison.Aggravant — le CHANGELOG affirme le contraire de la réalité.
CHANGELOG.md:21/CHANGELOG.fr.md:9:Les KPI cards et l'overlay sont des widgets transactionnels et ne respectent pas le filtre. Cette note ship aux utilisateurs — elle doit être vraie.
Correctif (court, ~6 lignes, motif M1 déjà en place) : ajouter
accountIds?: number[]àfetchMonthlyFlowsetfetchSeasonality, appliquerconst p = inPlaceholders(accountIds, N)puisAND source_id IN (${p})(colonnetransactions.source_id, cf.getExpensesByCategory:80), et transmettreaccountIdsdepuis les deux appels degetCartesSnapshot:1264-1265. Le helperinPlaceholdersretournenullsur filtre vide → requête byte-identique quand aucun compte n'est sélectionné (aucune régression du cas défaut). Un test dansreportService.cartes.test.tssur le passage d'accountIdsaux 4 branches verrouillerait le contrat.Alternative acceptable si filtrer les séries est délibérément différé : rendre l'UI + le CHANGELOG honnêtes — dire explicitement que les KPIs et l'overlay restent « tous comptes » pour l'instant (et idéalement une petite affordance UI pour que l'utilisateur ne soit pas induit en erreur). Moins bien, mais au moins la note de version cesse d'être fausse. À trancher par toi.
Non bloquant (suggestions)
Resolves #279: l'issue ne se fermera pas au merge. À ajouter.getDashboardSummarydevient du code mort (0 consommateur après la PR). Conservé sciemment ; ok, mais à noter pour un futur ménage.CategoryBarChart: masquage par clic-droit seulement (plus de légende cliquable), infobulle sans « % du total » vs l'ancien camembert. Vérifier que la perte est acceptée.CartesPeriodModeToggle) non exposé — déjà noté comme différé par l'auteur, ok.Une fois les séries filtrées (ou le CHANGELOG corrigé) + le
Resolves #279ajouté, cette PR est prête. Elle est solide à 95 %./pr-review — PR #287 : suivi du fix (blocage levé)
Verdict : RESOLVED → APPROVE — commit
9ee5ad3.Le point bloquant #1 (incohérence de filtrage I7) est corrigé.
getCartesSnapshotpropageaitaccountIdsuniquement aux sous-rapports top-movers et budget ; les sériesfetchMonthlyFlows(KPIs, sparklines, overlay 12 mois) etfetchSeasonalityl'ignoraient. Sur le Dashboard — première page à exposer ce filtre par-dessus ces séries — les KPI affichaient des totaux non filtrés pendant que les barres/tendance/top-movers respectaient le filtre.Correctif :
reportService.ts—fetchMonthlyFlowsetfetchSeasonalityacceptent unaccountIds?: number[]optionnel et ajoutent une clause paramétréesource_id IN (...)(viainPlaceholders, aux index$3/$4) ;getCartesSnapshotles câble. Doc-comment mis à jour.Vérification :
reportService.cartes.test.ts: filtre propagé à la série mensuelle (source_id IN ($3, $4)) et à la saisonnalité (source_id IN ($4, $5)) ; le test de régression « aucunsource_idsans filtre » tient (clause ajoutée seulement siaccountIdsprésent).tsc --noEmitclean · 798 vitest verts (47 fichiers).Resolves #279ajouté au commit (l'issue se fermera au merge).Suggestion non bloquante restante :
getDashboardSummarydevient du code mort — à retirer dans un suivi.Mergée localement dans
maindans le cadre de la pile M2 (a982f9e) — merge--no-ff+ résolution des conflits CHANGELOG (union #277/#278/#279). Forgejo ne marque pas les merges locaux comme merged ; fermeture manuelle. Issue liée auto-fermée viaResolves #N. Tip cumulé validé : tsc + vite build, 811 vitest, cargo check + 98 tests Rust.Pull request closed