feat(reports): tendance par catégorie = analyse de résultat (sections + résultat net par mois) #257

Closed
maximus wants to merge 0 commits from issue-256-tendance-resultat into main
Owner

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 : projette COALESCE(c.type, 'expense') AS category_type dans les deux SELECT (top-N + pivot) et remonte une map additive types (nom de catégorie → type), construite depuis les lignes top-N comme colors. L'ordre, les filtres et l'agrégation ABS(SUM()) sont inchangés → widget Dashboard (useDashboard) et CategoryOverTimeChart intacts.
  • shared/types : types ajouté à CategoryOverTimeData (+ types: {} dans les états initiaux des deux hooks).
  • Nouveau module pur 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 pur Totals/sumLeaves du 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).
  • i18n : ajout de reports.compare.resultNet / reports.compare.resultBeforeTransfers (FR + EN) ; réutilisation des clés existantes reports.compare.sections.* et total{Income,Expenses,Transfers}, common.total, budget.category.
  • CHANGELOG (Modifié / Changed, #256).

Points à noter

  • Ordre income-first : l'issue demande « Revenus → Dépenses → Transferts ». La constante existante COMPARE_TYPE_ORDER est en réalité expense-first ({ expense:0, income:1, transfer:2 }, le compare rend Dépenses en premier). Pour honorer l'ordre demandé, un OVER_TIME_TYPE_ORDER local 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).
  • Transferts (magnitudes absolues) : comme getCategoryOverTime renvoie des magnitudes ABS(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ègle reports/ (scratch autopilot) n'était pas ancrée et masquait tout nouveau fichier sous src/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 nouveau overTimeResults.test.ts (8 cas du réducteur pur + garde d'ordre) et les assertions types mises à jour dans reportService.test.ts.

Resolves #256

## 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`** : projette `COALESCE(c.type, 'expense') AS category_type` dans les deux SELECT (top-N + pivot) et remonte une map **additive** `types` (nom de catégorie → type), construite depuis les lignes top-N comme `colors`. L'ordre, les filtres et l'agrégation `ABS(SUM())` sont inchangés → widget Dashboard (`useDashboard`) et `CategoryOverTimeChart` intacts. - **`shared/types`** : `types` ajouté à `CategoryOverTimeData` (+ `types: {}` dans les états initiaux des deux hooks). - **Nouveau module pur `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 pur `Totals`/`sumLeaves` du 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). - **i18n** : ajout de `reports.compare.resultNet` / `reports.compare.resultBeforeTransfers` (FR + EN) ; réutilisation des clés existantes `reports.compare.sections.*` et `total{Income,Expenses,Transfers}`, `common.total`, `budget.category`. - **CHANGELOG** (Modifié / Changed, #256). ## Points à noter - **Ordre income-first** : l'issue demande « Revenus → Dépenses → Transferts ». La constante existante `COMPARE_TYPE_ORDER` est en réalité **expense-first** (`{ expense:0, income:1, transfer:2 }`, le compare rend Dépenses en premier). Pour honorer l'ordre demandé, un `OVER_TIME_TYPE_ORDER` local 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). - **Transferts (magnitudes absolues)** : comme `getCategoryOverTime` renvoie des magnitudes `ABS(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ègle `reports/` (scratch autopilot) n'était pas ancrée et masquait tout nouveau fichier sous `src/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 nouveau `overTimeResults.test.ts` (8 cas du réducteur pur + garde d'ordre) et les assertions `types` mises à jour dans `reportService.test.ts`. Resolves #256
maximus added 1 commit 2026-07-06 00:16:36 +00:00
feat(reports): category-over-time table as result analysis (sections + net result per month)
All checks were successful
PR Check / rust (pull_request) Successful in 22m9s
PR Check / frontend (pull_request) Successful in 2m28s
82040abc74
Apply the compare report's "result analysis" presentation to Trends -> By
category (table view). Categories are grouped into Income -> Expenses ->
Transfers sections with per-month and total subtotals; the old mixed "Total"
row (which summed absolute magnitudes into a meaningless figure) is replaced by
a Net result row per month (income - expenses + transfers) plus a Result before
transfers row shown only when transfers exist, both coloured by sign. Category
cells stay neutral levels.

- reportService.getCategoryOverTime: project COALESCE(c.type,'expense') in both
  SELECTs and return an additive `types` map (built from the top-N rows, like
  `colors`). Dashboard widget and CategoryOverTimeChart are untouched.
- shared types: add `types` to CategoryOverTimeData.
- new pure module overTimeResults.ts (computeOverTimeResults) with unit tests -
  the per-month result reducer, mirroring the compare's pure Totals helper.
- i18n: add reports.compare.resultNet / resultBeforeTransfers (FR+EN); reuse the
  existing sections.* and total* keys.
- .gitignore: anchor the autopilot `reports/` rule to `/reports/` so it no
  longer swallows new files under src/components/reports/.

Section order is income-first (Revenus -> Depenses -> Transferts), per the
issue; the existing COMPARE_TYPE_ORDER is expense-first, so a local
OVER_TIME_TYPE_ORDER is used rather than reusing it.

Resolves #256

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

/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_type dans le service est strictement additive (chart / widget Dashboard intacts), le réducteur pur computeOverTimeResults est 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)

  1. 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 — et ComparePeriodTable construit ses sections depuis des lignes déjà triées par cette constante (reportService.ts:634-638, sections income → expense → transfer). Le nouveau OVER_TIME_TYPE_ORDER est 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.)

  2. 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.

  3. Transferts en magnitudes dans le net (limite connue, déjà signalée). getCategoryOverTime renvoie des ABS(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 donne ABS(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é.

  4. Le bucket « Other » est typé en dépense dans le net. computeOverTimeResults classe 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 le topN=50 par 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.

  5. Projection SQL morte dans la requête mensuelle. getCategoryOverTime sélectionne désormais COALESCE(c.type,'expense') AS category_type aussi dans la ventilation mensuelle (reportService.ts:~148) et l'annote dans le type de ligne, mais le pivot ne la lit jamais (la map types est bâtie uniquement depuis topCategories). Sans danger (le GROUP BY month, t.category_id reste valide, c.type étant fonctionnellement déterminé par category_id), mais cette colonne et son annotation pourraient être retirées de la 2ᵉ requête.

Points positifs

  • Le fix .gitignore (reports//reports/) est correct et nécessaire : la règle non ancrée aurait ignoré tout nouveau fichier sous src/components/reports/ (les fichiers déjà suivis restant intacts). Sans ce fix, overTimeResults.ts et son test n'auraient pas pu être commités sans -f.
  • Changement de service strictement additif ; types: {} ajouté aux initial states des deux hooks pour satisfaire le champ requis → aucune régression sur le chart / Dashboard.
  • Réducteur pur bien isolé et testé (ordre income-first, net, collapse sans transferts, défaut expense, sections vides + transfert seul, exclusion des masquées, mois absent = 0). Coloring des lignes de résultat cohérent avec le compare (inline style sign-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 types mises à jour). i18n : OK (FR+EN). Migrations : aucune.

## /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_type` dans le service est strictement additive (chart / widget Dashboard intacts), le réducteur pur `computeOverTimeResults` est 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) 1. **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** — et `ComparePeriodTable` construit ses sections depuis des lignes déjà triées par cette constante (`reportService.ts:634-638`, sections income → expense → transfer). Le nouveau `OVER_TIME_TYPE_ORDER` est 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.) 2. **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. 3. **Transferts en magnitudes dans le net (limite connue, déjà signalée).** `getCategoryOverTime` renvoie des `ABS(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 donne `ABS(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é. 4. **Le bucket « Other » est typé en dépense dans le net.** `computeOverTimeResults` classe 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 le `topN=50` par 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. 5. **Projection SQL morte dans la requête mensuelle.** `getCategoryOverTime` sélectionne désormais `COALESCE(c.type,'expense') AS category_type` aussi dans la ventilation mensuelle (`reportService.ts:~148`) et l'annote dans le type de ligne, mais le pivot ne la lit jamais (la map `types` est bâtie uniquement depuis `topCategories`). Sans danger (le `GROUP BY month, t.category_id` reste valide, `c.type` étant fonctionnellement déterminé par `category_id`), mais cette colonne et son annotation pourraient être retirées de la 2ᵉ requête. ### Points positifs - Le fix `.gitignore` (`reports/` → `/reports/`) est correct et **nécessaire** : la règle non ancrée aurait ignoré tout nouveau fichier sous `src/components/reports/` (les fichiers déjà suivis restant intacts). Sans ce fix, `overTimeResults.ts` et son test n'auraient pas pu être commités sans `-f`. - Changement de service strictement additif ; `types: {}` ajouté aux initial states des deux hooks pour satisfaire le champ requis → aucune régression sur le chart / Dashboard. - Réducteur pur bien isolé et testé (ordre income-first, net, collapse sans transferts, défaut expense, sections vides + transfert seul, exclusion des masquées, mois absent = 0). Coloring des lignes de résultat cohérent avec le compare (inline `style` sign-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 `types` mises à jour). i18n : OK (FR+EN). Migrations : aucune.*
Author
Owner

Mergée localement dans main (merge commit 4497676). Fermée manuellement — un merge local ne marque pas la PR « merged » côté Forgejo.

Mergée localement dans `main` (merge commit 4497676). Fermée manuellement — un merge local ne marque pas la PR « merged » côté Forgejo.
maximus closed this pull request 2026-07-06 00:55:58 +00:00
All checks were successful
PR Check / rust (pull_request) Successful in 22m9s
PR Check / frontend (pull_request) Successful in 2m28s

Pull request closed

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#257
No description provided.