feat(reports): tendance par catégorie = analyse de résultat (sections + résultat net par mois) #257
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#257
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-256-tendance-resultat"
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?
Objectif
Applique au tableau Rapports → Tendances → Par catégorie (vue Table,
CategoryOverTimeTable) la présentation « analyse de résultat » livrée sur le rapport compare (#253).Changements
reportService.getCategoryOverTime: projetteCOALESCE(c.type, 'expense') AS category_typedans les deux SELECT (top-N + pivot) et remonte une map additivetypes(nom de catégorie → type), construite depuis les lignes top-N commecolors. L'ordre, les filtres et l'agrégationABS(SUM())sont inchangés → widget Dashboard (useDashboard) etCategoryOverTimeChartintacts.shared/types:typesajouté àCategoryOverTimeData(+types: {}dans les états initiaux des deux hooks).overTimeResults.ts(computeOverTimeResults) + tests : groupe les catégories visibles en sections par type, calcule les sous-totaux par mois et par colonne Total, le Résultat net par mois (revenus − dépenses + transferts) et le Résultat avant transferts (revenus − dépenses). Miroir du helper purTotals/sumLeavesdu compare. Testé en isolation (pas de harness de rendu React dans le projet).CategoryOverTimeTable.tsx: sections Revenus → Dépenses → Transferts, sous-total par section, et remplacement de l'ancienne ligne « Total » mélangée (qui sommait des magnitudes absolues = chiffre sans signification) par la ligne Résultat net par mois, colorée par signe (surplus vert / déficit rouge), + Résultat avant transferts si des transferts existent. Les cellules de catégorie restent des niveaux neutres (pas de couleur directionnelle).reports.compare.resultNet/reports.compare.resultBeforeTransfers(FR + EN) ; réutilisation des clés existantesreports.compare.sections.*ettotal{Income,Expenses,Transfers},common.total,budget.category.Points à noter
COMPARE_TYPE_ORDERest en réalité expense-first ({ expense:0, income:1, transfer:2 }, le compare rend Dépenses en premier). Pour honorer l'ordre demandé, unOVER_TIME_TYPE_ORDERlocal income-first est utilisé plutôt que de réutiliser la constante. Conséquence : ce tableau est income-first alors que le compare reste expense-first. Si une cohérence stricte est souhaitée, il faudra aligner l'un sur l'autre (décision produit).getCategoryOverTimerenvoie des magnitudesABS(SUM())(nécessaire au graphe/Dashboard, non modifié), le transfert contribue sa magnitude positive au Résultat net. Pour le cas courant (catégorie de transfert équilibrée → ~0) c'est correct ; un transfert déséquilibré à une jambe perd son signe — même limite non bloquante que le compare (cf. STATE.md). La ligne « Résultat avant transferts » donne le chiffre propre..gitignore: la règlereports/(scratch autopilot) n'était pas ancrée et masquait tout nouveau fichier soussrc/components/reports/— ancrée en/reports/(les fichiers source déjà suivis n'étaient pas affectés).Validation
npm run build(tsc + vite) : exit 0.npx vitest run: 689 tests verts (40 fichiers), dont le nouveauoverTimeResults.test.ts(8 cas du réducteur pur + garde d'ordre) et les assertionstypesmises à jour dansreportService.test.ts.Resolves #256
/pr-review — PR #257 (Resolves #256)
Verdict : APPROVE — aucun bloquant ; 5 remarques non bloquantes.
Résumé : La transformation du tableau « Par catégorie » en analyse de résultat est correcte et bien testée. La projection de
category_typedans le service est strictement additive (chart / widget Dashboard intacts), le réducteur purcomputeOverTimeResultsest couvert par 8 cas, l'i18n est ajoutée dans les deux langues, le CHANGELOG est à jour, aucune migration DB. Build tsc + 689 vitest annoncés verts. Les points ci-dessous sont des améliorations et une correction de la description — pas des blocages.Remarques (non bloquantes)
La description de la PR se trompe sur
COMPARE_TYPE_ORDER. Le body affirme que la constante est expense-first ({expense:0,income:1,transfer:2}) et que le compare rend Dépenses en premier, d'où une divergence assumée. En réalitéCOMPARE_TYPE_ORDER(reportService.ts:452) vaut{ income:0, expense:1, transfer:2 }— income-first — etComparePeriodTableconstruit ses sections depuis des lignes déjà triées par cette constante (reportService.ts:634-638, sections income → expense → transfer). Le nouveauOVER_TIME_TYPE_ORDERest donc identique àCOMPARE_TYPE_ORDER, et les deux rapports sont en fait cohérents (income-first tous les deux). Le code est correct (income-first = ce que demande #256) ; seul le raisonnement du body est faux — aucune « décision produit » n'est en attente pour les réconcilier, ils sont déjà alignés. À corriger dans la description pour ne pas induire en erreur plus tard. (Accessoirement, la constante locale duplique celle du service ; une constante partagée serait possible, mais le cloisonnement data-layer / composant rend le doublon défendable.)Placement de « Résultat avant transferts » différent du compare. Dans le compare, cette ligne est rendue entre la section Dépenses et la section Transferts (
ComparePeriodTable.tsx:382-388, commentaire explicite « shown before the transfers section »). Ici elle est rendue après toutes les sections, transferts compris (sous le sous-total Transferts, juste au-dessus du Résultat net). Pour un tableau censé « se lire comme le compare », placer « avant transferts » au-dessus de la section Transferts est plus logique (résultat d'exploitation → puis transferts → puis net). Polish UX.Transferts en magnitudes dans le net (limite connue, déjà signalée).
getCategoryOverTimerenvoie desABS(SUM()), donc le terme transfert du net est une magnitude positive, contrairement au compare qui somme des transferts signés (compareResults.ts:71-85). Un transfert équilibré gardé dans une seule catégorie et un seul mois donneABS(SUM)=0(OK) ; un transfert à une jambe ou éclaté sur deux catégories gonfle le net. La ligne « Résultat avant transferts » reste le chiffre propre. Conforme à la caveat déjà documentée du compare (STATE.md) — signalé pour visibilité.Le bucket « Other » est typé en dépense dans le net.
computeOverTimeResultsclasse toute catégorie inconnue (dont « Other », qui agrège les hors-top-N) en section Dépenses (overTimeResults.ts,typeOf), donc soustraite au net. Les deux appelants utilisent letopN=50par défaut (useTrends.ts:62,useDashboard.ts:111), donc « Other » n'apparaît qu'au-delà de 50 catégories dans la fenêtre — cas limite. Si un tel utilisateur a des revenus/transferts hors-top-N, ils sont comptés comme dépense dans le net. Hors scope de corriger ici, mais à garder en tête.Projection SQL morte dans la requête mensuelle.
getCategoryOverTimesélectionne désormaisCOALESCE(c.type,'expense') AS category_typeaussi dans la ventilation mensuelle (reportService.ts:~148) et l'annote dans le type de ligne, mais le pivot ne la lit jamais (la maptypesest bâtie uniquement depuistopCategories). Sans danger (leGROUP BY month, t.category_idreste valide,c.typeétant fonctionnellement déterminé parcategory_id), mais cette colonne et son annotation pourraient être retirées de la 2ᵉ requête.Points positifs
.gitignore(reports/→/reports/) est correct et nécessaire : la règle non ancrée aurait ignoré tout nouveau fichier soussrc/components/reports/(les fichiers déjà suivis restant intacts). Sans ce fix,overTimeResults.tset son test n'auraient pas pu être commités sans-f.types: {}ajouté aux initial states des deux hooks pour satisfaire le champ requis → aucune régression sur le chart / Dashboard.stylesign-based via--positive/--negative).Review adversariale automatisée. Sécurité : OK (requêtes paramétrées, pas de secret, pas d'injection). Tests : OK (8 nouveaux cas + assertions
typesmises à jour). i18n : OK (FR+EN). Migrations : aucune.Mergée localement dans
main(merge commit4497676). Fermée manuellement — un merge local ne marque pas la PR « merged » côté Forgejo.Pull request closed