649 lines
48 KiB
Markdown
649 lines
48 KiB
Markdown
## Spec — Refonte complete des rapports
|
||
|
||
> Date : 2026-04-13
|
||
> Projet : simpl-resultat
|
||
> Statut : Revised post-review (2026-04-13) — prêt pour implémentation
|
||
|
||
## Contexte
|
||
|
||
La page `/reports` actuelle expose cinq onglets (`trends`, `byCategory`, `overTime`, `budgetVsActual`, `dynamic`) pensés comme autant de vues analytiques indépendantes. L'ensemble souffre de trois limites :
|
||
|
||
1. **Pas de récit** — aucune vue ne répond à la question « qu'est-ce qui est important à savoir sur mes finances ce mois-ci ? ». L'utilisateur doit naviguer entre les onglets et reconstituer le tableau d'ensemble lui-même.
|
||
2. **Pivot surdimensionné** — le tableau croisé dynamique (`DynamicReport`) est puissant mais complexe, peu utilisé dans la pratique, et ajoute une dette visuelle/cognitive. Son usage se résume en réalité à « zoomer sur une catégorie ».
|
||
3. **Classification déconnectée** — les mots-clés s'éditent uniquement depuis `/categories`. Quand l'utilisateur voit une transaction mal classée dans un rapport, il doit quitter son contexte pour aller modifier la règle puis revenir.
|
||
|
||
La refonte garde la signature visuelle (palette couleurs catégorie + patterns SVG grayscale-friendly) mais réorganise le contenu autour de **quatre axes d'analyse** correspondant à quatre questions utilisateur :
|
||
|
||
| Rapport | Question utilisateur |
|
||
|--|--|
|
||
| Faits saillants | « Qu'est-ce qui a bougé ce mois ? » |
|
||
| Tendances | « Où je vais sur les 12 derniers mois ? » |
|
||
| Comparables | « Comment je me situe vs période précédente ou vs budget ? » |
|
||
| Analyse ponctuelle | « Montre-moi tout sur cette catégorie. » |
|
||
|
||
## Objectif
|
||
|
||
Refondre `/reports` en un hub unifié qui présente un aperçu des faits saillants + quatre rapports dédiés (tendances, comparables, faits saillants, zoom catégorie), avec toggle graphique/tableau partout, signature visuelle conservée, et édition contextuelle des mots-clés (clic droit sur transaction → preview → appliquer) pour améliorer la classification sans quitter le rapport.
|
||
|
||
> **🟢 ARCHITECTURE** — Hub + sous-routes est le bon move.
|
||
> URLs bookmarkables, back button natif, chaque page charge ses propres données, meilleur code-splitting possible.
|
||
> **Resolution :** Procéder, mais résoudre d'abord les 3 critiques architecture (structure pages plate, split useReports, mécanisme de partage de période) pour ne pas bâtir sur des bases désalignées.
|
||
|
||
## Scope
|
||
|
||
### IN
|
||
|
||
- Nouvelle page hub `/reports` affichant en haut un panneau "Faits saillants" (top mouvements, top transactions récentes, solde net mois courant + YTD) suivi de quatre cartes menant aux quatre sous-rapports.
|
||
- Quatre sous-pages :
|
||
- `/reports/highlights` — version détaillée des faits saillants
|
||
- `/reports/trends` — revenus vs dépenses (flux global) + évolution par catégorie
|
||
- `/reports/compare` — mois vs mois-1, année vs année-1, réel vs budget (mois et année), navigation tabulaire
|
||
- `/reports/category` — zoom sur une catégorie avec rollup automatique des sous-catégories
|
||
- Toggle **graphique ↔ tableau** sur tous les sous-rapports, défaut graphique, préférence mémorisée par rapport dans `localStorage`.
|
||
- Conservation de la signature visuelle : palette couleurs existante + patterns SVG (`chartPatterns.tsx`) + Recharts. Ajout de deux nouveaux types de charts : sparklines (pour les faits saillants) et donut chart (pour la répartition dans le zoom catégorie).
|
||
- Rollup automatique des sous-catégories dans le zoom catégorie (sélectionner `Alimentation` inclut toutes ses enfants, avec sous-total par enfant). Toggle « direct seulement » disponible.
|
||
- Édition contextuelle des mots-clés : clic droit sur n'importe quelle transaction (dans n'importe quel rapport ou dans la page Transactions) → menu « Ajouter ce libellé comme mot-clé pour catégorie X » → dialog preview des matches → Appliquer / Annuler.
|
||
- Retrait du tableau croisé dynamique de l'UI : les composants `DynamicReport*` restent dans le code mais derrière un feature flag désactivé par défaut (ou route cachée `/reports/_pivot`), au cas où un usage avancé réémergerait.
|
||
- Période par défaut à l'ouverture du hub : année civile en cours (1er janvier → 31 décembre).
|
||
- Traductions FR/EN complètes pour toutes les nouvelles clés.
|
||
- Mise à jour de `CHANGELOG.md` et `CHANGELOG.fr.md` sous `## [Unreleased]`.
|
||
|
||
### OUT (explicitement exclu)
|
||
|
||
- Projection / prévision des prochains mois (pas de ML, pas d'extrapolation).
|
||
- Moyennes mobiles (mentionnées mais non retenues pour ce sprint).
|
||
- Anomalies / alertes automatiques (pas de détection "dépense inhabituelle").
|
||
- KPIs dérivés (taux d'épargne, ratio fixe/variable) — à reconsidérer plus tard.
|
||
- Édition des mots-clés directement inline dans le panneau du zoom catégorie (on se limite au clic droit contextuel).
|
||
- Migration de données : aucune nouvelle table SQL, aucun changement de schéma.
|
||
- Remplacement de Recharts par une autre librairie.
|
||
- Rapports par fournisseur / par tag / par compte bancaire (hors scope).
|
||
|
||
## Design
|
||
|
||
### UX / Interface
|
||
|
||
#### Hub `/reports`
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Rapports [Période : 2026 ▾] │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ FAITS SAILLANTS │
|
||
│ ┌──────────┬──────────┬────────────┬──────────────┐ │
|
||
│ │ Solde │ Solde │ Top hausse │ Top baisse │ │
|
||
│ │ avril │ YTD │ Restos │ Épicerie │ │
|
||
│ │ +312 $ │ +1 845 $ │ +240 $ │ -85 $ │ │
|
||
│ │ [spark] │ [spark] │ vs mars │ vs mars │ │
|
||
│ └──────────┴──────────┴────────────┴──────────────┘ │
|
||
│ │
|
||
│ Top 5 transactions récentes │
|
||
│ • 2026-04-10 Loyer avril 1 450,00 $ │
|
||
│ • 2026-04-08 Remboursement prêt 680,00 $ │
|
||
│ • ... │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ EXPLORER │
|
||
│ ┌─────────────┬─────────────┬─────────────┬──────────┐ │
|
||
│ │ Tendances │ Comparables │ Faits saill.│ Analyse │ │
|
||
│ │ 📈 │ ⚖ │ ⭐ │ 🔍 │ │
|
||
│ └─────────────┴─────────────┴─────────────┴──────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- Le hub charge un `ReportsHighlights` résumé en haut, même données que `/reports/highlights` mais layout condensé.
|
||
- Quatre tuiles de navigation au bas mènent aux sous-pages.
|
||
- Sélecteur de période global en haut à droite, partagé avec les sous-rapports (contexte conservé à la navigation).
|
||
|
||
> **🔴 ARCHITECTURE** — Mécanisme de partage de période non spécifié.
|
||
> Avec 4 routes séparées, chaque page monte un `useReports` neuf → l'état local est perdu à chaque navigation. « Contexte conservé » n'est pas défini.
|
||
> **Resolution :** Utiliser une query string `?from=...&to=...&period=2026` (simple, bookmarkable, pas de contexte global — cohérent avec le reste du projet qui n'utilise pas de contexte React pour l'état UI).
|
||
|
||
#### `/reports/highlights`
|
||
|
||
Version détaillée des faits saillants :
|
||
- Bloc **Soldes** : grandes tuiles avec solde net mois courant + YTD, chacune avec un sparkline 12 mois.
|
||
- Bloc **Top mouvements** : tableau triable des catégories avec la plus forte variation absolue ($) ou relative (%) vs mois précédent. Toggle `$` / `%`.
|
||
- Bloc **Top transactions récentes** : liste des 10 plus grosses transactions des 30 derniers jours (configurable 30/60/90 jours).
|
||
- Toggle graphique/tableau s'applique aux tops (barres horizontales ou tableau).
|
||
|
||
#### `/reports/trends`
|
||
|
||
Deux sous-vues accessibles par un mini-toggle interne :
|
||
- **Flux global** — AreaChart revenus/dépenses/solde net sur la période (reprend `MonthlyTrendsChart`, maintenu tel quel visuellement). Version tableau : `MonthlyTrendsTable`.
|
||
- **Par catégorie** — sélection multi-catégories + courbes d'évolution (adapte `CategoryOverTimeChart`). Version tableau : `CategoryOverTimeTable`.
|
||
|
||
Un seul sélecteur graphique/tableau en haut qui s'applique à la sous-vue affichée.
|
||
|
||
#### `/reports/compare`
|
||
|
||
Trois modes accessibles par un tab bar secondaire :
|
||
- **Mois vs mois précédent** — tableau catégories × 2 colonnes + écart $ / % ; version graphique = diverging bar chart centré sur 0.
|
||
- **Année vs année précédente** — même principe sur 12 mois vs 12 mois.
|
||
- **Réel vs budget** — reprend la logique de `BudgetVsActualTable` existante ; toggle mensuel / annuel (YTD).
|
||
|
||
Navigation entre les trois modes conserve la période et les filtres.
|
||
|
||
#### `/reports/category`
|
||
|
||
Vue single-category :
|
||
- En haut : combobox de sélection de catégorie + toggle **« inclure sous-catégories »** (activé par défaut).
|
||
- Zone principale :
|
||
- **Donut chart** de la répartition par sous-catégorie (ou pie si pas de rollup), couleurs de catégorie.
|
||
- Chart d'évolution mensuelle de la catégorie sur la période (AreaChart).
|
||
- Tableau des transactions de la catégorie (sortable, filtrable par date/montant).
|
||
- Toggle graphique/tableau cache/montre les visualisations.
|
||
|
||
#### Édition contextuelle des mots-clés
|
||
|
||
Trigger : clic droit sur une ligne de transaction dans n'importe quel tableau (rapports, zoom catégorie, mais aussi éventuellement `/transactions`).
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ Ajouter le mot-clé « METRO » ? │
|
||
│ │
|
||
│ Catégorie cible : [Alimentation ▾] │
|
||
│ Priorité : [100 ] │
|
||
│ │
|
||
│ Ce mot-clé matchera aussi : │
|
||
│ ☑ 2026-03-15 METRO #123 45,00 $ │
|
||
│ ☑ 2026-03-02 METRO PLUS 67,20 $ │
|
||
│ ☐ 2026-02-18 METROPOLITAIN 12,00 $ │
|
||
│ │
|
||
│ ⚠ 3 transactions seront recatégorisées │
|
||
│ │
|
||
│ [Annuler] [Appliquer] │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
**Implémentation arrêtée** (post-review sécurité) :
|
||
|
||
- **Normalisation** : utiliser `normalizeDescription` et `buildKeywordRegex` depuis `categorizationService.ts` — ces helpers sont actuellement privés, à exporter dans Issue #6.
|
||
- **Validation longueur** : keyword obligatoire entre 2 et 64 caractères après `.trim()`, rejet whitespace-only. Prévient ReDoS (CWE-1333).
|
||
- **Preview via SQL paramétrée** : `SELECT ... FROM transactions WHERE description LIKE ?1` (jamais d'interpolation de chaîne), puis filtrage en mémoire avec le regex compilé par `buildKeywordRegex`. Prévient injection SQL (CWE-89).
|
||
- **Affichage limité à 50 matches** ; au-delà, une checkbox explicite « Appliquer aussi aux N-50 transactions non affichées » s'affiche (off par défaut).
|
||
- **Appliquer** = exécution dans une **transaction SQL englobante** (`BEGIN; INSERT keywords; UPDATE transactions; COMMIT;`) via `tauri-plugin-sql`, avec rollback + toast erreur en cas d'échec. Application uniquement aux lignes **cochées visibles** (sauf si l'utilisateur a explicitement coché l'option des N-50 non affichées).
|
||
- **Comportement « mot-clé déjà existant pour autre catégorie »** : `UPDATE keywords SET category_id=? WHERE keyword=?` + re-run de la catégorisation **uniquement** sur les matches visibles cochés (jamais rétroactif sur l'historique complet).
|
||
- **Rendu XSS-safe** : les descriptions de transaction sont rendues comme enfants React (`{tx.description}`) — jamais `dangerouslySetInnerHTML`. Troncature via CSS uniquement (CWE-79).
|
||
- **Annuler** = aucune modification, dialog fermé.
|
||
|
||
> **🔴 TECHNIQUE** — `normalizeString` n'existe pas dans `categorizationService.ts`.
|
||
> Le service n'expose que `buildKeywordRegex` ; il existe un `normalizeDescription` **privé** (non exporté). L'import référencé dans la spec est invalide.
|
||
> **Resolution :** Ajouter à Issue 5 une tâche « Exporter `normalizeDescription` et `buildKeywordRegex` depuis `categorizationService.ts` » et corriger le nom dans la spec.
|
||
|
||
> **🔴 SECURITE** — Preview SQL doit être paramétrée, jamais interpoler le mot-clé.
|
||
> SQLite n'a pas d'opérateur regex natif ; une implémentation naïve construirait un `LIKE '%' || keyword || '%'` interpolé à partir d'un texte de transaction potentiellement malveillant (CSV importé), ouvrant une injection SQL locale.
|
||
> **Resolution :** Charger les candidats via SQL paramétré (`LIKE ?1` ou scan complet via `tauri-plugin-sql` binding) puis filtrer en mémoire avec le regex compilé par `buildKeywordRegex`. Jamais de `string || keyword`.
|
||
> *Ref : OWASP A03:2021 / CWE-89*
|
||
|
||
> **🔴 SECURITE** — ReDoS / absence de cap sur la longueur du mot-clé.
|
||
> `buildKeywordRegex` échappe les metacharactères mais ne limite pas la longueur. Un mot-clé de 5000 caractères serait compilé puis rejoué à chaque import — freeze garanti de l'app.
|
||
> **Resolution :** Dans `AddKeywordDialog`, valider `keyword.trim().length` entre 2 et 64 avant INSERT ; rejeter whitespace-only. Ajouter cette règle à la table Edge cases.
|
||
> *Ref : CWE-1333*
|
||
|
||
> **🟡 SECURITE** — L'apply doit tourner en une seule transaction SQL (BEGIN/COMMIT).
|
||
> Le critère d'acceptation le mentionne mais la narration décrit INSERT + UPDATE séquentiels. Un crash entre les deux laisse un keyword orphelin ou des transactions non-recatégorisées — sur une app privacy-first sans backup par défaut, la confiance est ébranlée.
|
||
> **Resolution :** Envelopper explicitement INSERT keywords + UPDATE transactions dans `BEGIN / COMMIT / ROLLBACK` via `tauri-plugin-sql` ; surfacer les erreurs de rollback via un toast.
|
||
> *Ref : CWE-662*
|
||
|
||
> **🟡 SECURITE** — « Preview 50 + apply sur tous » viole le contrôle utilisateur.
|
||
> La Edge case dit « Dialog limite l'affichage à 50 matches avec + N autres ; l'UPDATE s'applique bien à tous ». L'utilisateur coche 50 lignes, l'app en modifie 300 sans undo.
|
||
> **Resolution :** Soit appliquer uniquement aux lignes cochées réellement affichées, soit exiger une confirmation explicite « Appliquer aussi aux N-50 transactions non affichées » (checkbox off par défaut).
|
||
> *Ref : OWASP ASVS V1.11.2*
|
||
|
||
> **🟡 SECURITE** — « Remplacer » un mot-clé existant n'est pas défini.
|
||
> La edge case dit « Ce mot-clé existe déjà pour catégorie X, remplacer ? » mais ne précise pas si ça UPDATE silencieusement l'ancien keyword (ce qui re-catégoriserait rétroactivement des années de transactions) ou crée un doublon.
|
||
> **Resolution :** Décider explicitement : `UPDATE keywords SET category_id=? WHERE keyword=?` + re-run de la catégorisation uniquement sur les matches visibles (pas sur l'historique). Écrire la décision dans la spec.
|
||
|
||
> **🟡 ARCHITECTURE** — `AddKeywordDialog` et `TransactionContextMenu` ne sont pas du domaine "reports".
|
||
> La spec dit explicitement qu'ils seront utilisés dans `/transactions` et les rapports. Les placer sous `components/reports/shared/` viole SRP.
|
||
> **Resolution :** Placer `AddKeywordDialog` dans `components/categories/` (domaine édition mot-clé) et `TransactionContextMenu` dans `components/shared/` (cross-page).
|
||
|
||
> **🟢 SECURITE** — Rendre les descriptions de transaction en enfants React uniquement.
|
||
> Les libellés affichés dans le menu et le dialog viennent de CSV imports (untrusted). Une utilisation naïve de `dangerouslySetInnerHTML` ou de `title=` avec HTML réintroduirait XSS dans le webview Tauri.
|
||
> **Resolution :** Spécifier dans la spec que les descriptions sont rendues comme enfants React (jamais `dangerouslySetInnerHTML`) et tronquées via CSS uniquement.
|
||
> *Ref : CWE-79*
|
||
|
||
### Données
|
||
|
||
**Aucune migration SQL.** Toutes les requêtes s'appuient sur les tables existantes :
|
||
- `transactions` — agrégats mensuels, tops, filtres date/catégorie.
|
||
- `categories` — hiérarchie pour rollup, couleurs, types.
|
||
- `keywords` — insertion nouvelle règle via dialog contextuel.
|
||
- `budget_entries` — réel vs budget.
|
||
- `import_sources` — filtres optionnels.
|
||
|
||
Nouveaux endpoints dans `reportService.ts` (SQL strictement paramétré, jamais d'interpolation) :
|
||
|
||
| Fonction | Rôle |
|
||
|--|--|
|
||
| `getHighlights(from, to)` | Retourne `{ netBalanceCurrent, netBalanceYtd, monthlyBalanceSeries, topMovers: {category, deltaAbs, deltaPct}[], topTransactions: Transaction[] }` |
|
||
| `getCompareMonthOverMonth(year, month)` | Retourne `CategoryDelta[]` pour mois cible vs mois précédent |
|
||
| `getCompareYearOverYear(year)` | Retourne `CategoryDelta[]` pour année cible vs année précédente |
|
||
| `getCategoryZoom(categoryId, from, to, includeSubcategories)` | Retourne `{ rollupTotal, byChild, monthlyEvolution, transactions }` |
|
||
|
||
**`getCategoryZoom` — cycle guard obligatoire** : le rollup des sous-catégories passe par une CTE SQLite récursive **bornée** pour se protéger contre d'éventuels cycles dans `parent_id` :
|
||
|
||
```sql
|
||
WITH RECURSIVE cat_tree(id, depth) AS (
|
||
SELECT id, 0 FROM categories WHERE id = ?1
|
||
UNION ALL
|
||
SELECT c.id, ct.depth + 1
|
||
FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id
|
||
WHERE ct.depth < 5
|
||
)
|
||
SELECT ... WHERE category_id IN (SELECT id FROM cat_tree);
|
||
```
|
||
|
||
Un test unitaire avec fixture cyclique (A→B→A) doit valider la terminaison.
|
||
|
||
Le service `getBudgetVsActualData` de `budgetService.ts` est réutilisé tel quel pour le mode réel-vs-budget.
|
||
|
||
> **🔴 SECURITE** — `getCategoryZoom` rollup récursif sans garde-fou cyclique.
|
||
> La table `categories` autorise n'importe quel `parent_id` (pas de check FK contre cycles). Une donnée malformée A→B→A fait tourner un walk récursif à l'infini et fige l'UI. `getCategoryDepth` existant a déjà ce risque latent.
|
||
> **Resolution :** Implémenter le rollup via une CTE SQLite récursive bornée (`WITH RECURSIVE ... WHERE depth < 5`) ou tracer un `Set<visited>` en JS. Ajouter un test unitaire avec une fixture cyclique.
|
||
> *Ref : CWE-835*
|
||
|
||
### Architecture
|
||
|
||
#### Nouvelle structure des fichiers
|
||
|
||
Convention : **`src/pages/` et `src/components/reports/` restent plats** (aucun sous-dossier par domaine), cohérent avec le reste du projet. Distinction par préfixe de nom.
|
||
|
||
```
|
||
src/pages/ # plat, comme le reste du projet
|
||
├── ReportsPage.tsx # refonte : devient le hub
|
||
├── ReportsHighlightsPage.tsx # NOUVEAU
|
||
├── ReportsTrendsPage.tsx # NOUVEAU
|
||
├── ReportsComparePage.tsx # NOUVEAU
|
||
└── ReportsCategoryPage.tsx # NOUVEAU
|
||
|
||
src/components/reports/ # plat, préfixes par domaine
|
||
# Hub
|
||
├── HubHighlightsPanel.tsx # NOUVEAU — panneau condensé pour le hub
|
||
├── HubReportNavCard.tsx # NOUVEAU — les 4 tuiles de navigation
|
||
├── HubNetBalanceTile.tsx # NOUVEAU — tuile solde + sparkline
|
||
├── HubTopMoversTile.tsx # NOUVEAU
|
||
├── HubTopTransactionsTile.tsx # NOUVEAU
|
||
# Highlights
|
||
├── HighlightsTopMoversTable.tsx # NOUVEAU
|
||
├── HighlightsTopMoversChart.tsx # NOUVEAU — diverging bar chart
|
||
├── HighlightsTopTransactionsList.tsx # NOUVEAU
|
||
# Compare
|
||
├── CompareModeTabs.tsx # NOUVEAU
|
||
├── ComparePeriodTable.tsx # NOUVEAU
|
||
├── ComparePeriodChart.tsx # NOUVEAU — diverging bar chart
|
||
├── CompareBudgetView.tsx # NOUVEAU — wrap BudgetVsActualTable
|
||
# Category zoom
|
||
├── CategoryZoomHeader.tsx # NOUVEAU — combobox + toggle rollup
|
||
├── CategoryDonutChart.tsx # NOUVEAU — template : dashboard/CategoryPieChart.tsx
|
||
├── CategoryEvolutionChart.tsx # NOUVEAU
|
||
├── CategoryTransactionsTable.tsx # NOUVEAU
|
||
# Shared (intra-reports)
|
||
├── ViewModeToggle.tsx # NOUVEAU — toggle graphique/tableau
|
||
├── Sparkline.tsx # NOUVEAU — mini chart Recharts
|
||
# EXISTANTS réutilisés tels quels
|
||
├── MonthlyTrendsChart.tsx # par TrendsPage (flux global)
|
||
├── MonthlyTrendsTable.tsx
|
||
├── CategoryOverTimeChart.tsx # par TrendsPage (par catégorie)
|
||
├── CategoryOverTimeTable.tsx
|
||
├── BudgetVsActualTable.tsx # wrapé par CompareBudgetView
|
||
├── CategoryBarChart.tsx
|
||
├── CategoryTable.tsx
|
||
└── ReportFilterPanel.tsx
|
||
|
||
# Autres emplacements hors src/components/reports/
|
||
src/components/shared/ContextMenu.tsx # NOUVEAU — shell générique (click-outside + Escape)
|
||
src/components/shared/ChartContextMenu.tsx # REFACTORÉ — compose ContextMenu
|
||
src/components/categories/AddKeywordDialog.tsx # NOUVEAU — domaine édition mot-clé, pas reports
|
||
|
||
# SUPPRIMÉS (pivot retiré franchement, git conserve l'historique)
|
||
src/components/reports/DynamicReport.tsx
|
||
src/components/reports/DynamicReportPanel.tsx
|
||
src/components/reports/DynamicReportTable.tsx
|
||
src/components/reports/DynamicReportChart.tsx
|
||
```
|
||
|
||
> **🔴 ARCHITECTURE** — `src/pages/reports/` casse la convention flat du projet.
|
||
> Aucune page du projet n'utilise de sous-dossier aujourd'hui (`src/pages/` est strictement plat : `DashboardPage`, `ImportPage`, `BudgetPage`, etc.). Introduire un sous-dossier pour un seul domaine crée une règle ad-hoc.
|
||
> **Resolution :** Garder `src/pages/` plat : nommer `ReportsHighlightsPage.tsx`, `ReportsTrendsPage.tsx`, `ReportsComparePage.tsx`, `ReportsCategoryPage.tsx` à côté de `ReportsPage.tsx`. Cohérent avec le reste du projet.
|
||
|
||
> **🟡 ARCHITECTURE** — Split `components/reports/` en sous-dossiers incohérent.
|
||
> La spec crée `hub/`, `highlights/`, `compare/`, `category/`, `shared/` mais laisse `MonthlyTrendsChart`, `BudgetVsActualTable`, `DynamicReport*` à la racine. Mix flat+nested ; les autres dossiers composants (`import/`, `dashboard/`, `profile/`) sont strictement plats.
|
||
> **Resolution :** Tout garder plat avec préfixes de nom (`HubNetBalanceTile`, `HighlightsTopMovers`, `CompareModeTabs`, `CategoryDonutChart`, etc.). Moins de churn git, cohérent avec le reste.
|
||
|
||
> **🟡 TECHNIQUE** — `TransactionContextMenu` duplique `ChartContextMenu` existant.
|
||
> `src/components/shared/ChartContextMenu.tsx` implémente déjà click-outside + Escape handling avec un shell menu réutilisable. La spec crée un nouveau composant from scratch sans le référencer.
|
||
> **Resolution :** Généraliser `ChartContextMenu` en `ContextMenu` réutilisable (items passés en children/props) et le réutiliser pour les transactions. Ajouter la refactorisation comme tâche explicite d'Issue 5.
|
||
|
||
> **🟢 TECHNIQUE** — Recharts 3.7 supporte déjà `<Pie innerRadius>` (donut).
|
||
> Vérifié : `recharts@^3.7.0` est installé et `src/components/dashboard/CategoryPieChart.tsx` rend déjà un `<Pie>` avec patterns. Le donut est juste `innerRadius`, pas de nouvelle dépendance.
|
||
> **Resolution :** Référencer `CategoryPieChart.tsx` comme template de démarrage pour `CategoryDonutChart.tsx` dans Issue 5.
|
||
|
||
#### Routing (`src/App.tsx`)
|
||
|
||
Nouvelles routes imbriquées :
|
||
```tsx
|
||
<Route path="/reports" element={<ReportsPage />} />
|
||
<Route path="/reports/highlights" element={<HighlightsPage />} />
|
||
<Route path="/reports/trends" element={<TrendsPage />} />
|
||
<Route path="/reports/compare" element={<ComparePage />} />
|
||
<Route path="/reports/category" element={<CategoryZoomPage />} />
|
||
```
|
||
|
||
#### Hooks par domaine (refonte de `useReports`)
|
||
|
||
Le hook monolithique `useReports` est splitté en **hooks par domaine**, conformément au pattern « useReducer par domaine » documenté dans CLAUDE.md. Chaque page monte uniquement son propre hook — pas de god-object ni de refetch de champs hors-section.
|
||
|
||
| Hook | Rôle |
|
||
|---|---|
|
||
| `useReportsPeriod` | Lit/écrit la période via **query string** (`?from=YYYY-MM-DD&to=YYYY-MM-DD`) avec `useSearchParams` de react-router. Bookmarkable. Par défaut : année civile en cours. |
|
||
| `useHighlights` | Fetch + state du rapport faits saillants, `useReducer` dédié |
|
||
| `useTrends` | idem pour tendances (sous-vue flux global / par catégorie) |
|
||
| `useCompare` | idem pour comparables (mode MoM / YoY / budget) |
|
||
| `useCategoryZoom` | idem pour le zoom catégorie (`zoomedCategoryId`, `rollupChildren`) |
|
||
|
||
Les préférences `viewMode` (chart / table) sont persistées par section dans `localStorage` via une `storageKey` passée en prop à `ViewModeToggle` (`reports-viewmode-highlights`, `-trends`, `-compare`, `-category`).
|
||
|
||
Pendant la transition (Issue #2), `useReports` conserve temporairement ses champs legacy (`monthlyTrends`, `categorySpending`, etc.) recâblés sur `useReportsPeriod`, pour que les 4 rapports existants continuent de fonctionner jusqu'à ce qu'Issues #3–6 les migrent. Une fois tous les rapports migrés (Issue #8), `useReports` est supprimé.
|
||
|
||
> **🔴 ARCHITECTURE** — Un seul hook partagé entre 4 routes = god-object.
|
||
> Avec 4 routes react-router, chaque page monte/démonte son propre hook ; un seul `useReports` portant `section + compareMode + trendsSubView + zoomedCategoryId + rollupChildren + period` perd le bénéfice du routing : chaque page refetche tout et les champs hors-section polluent l'état.
|
||
> **Resolution :** Splitter en hooks par domaine : `useReportsPeriod` (partagé via query string), `useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`. Cohérent avec le pattern « useReducer par domaine » documenté dans CLAUDE.md.
|
||
|
||
#### Suppression du pivot (tableau croisé dynamique)
|
||
|
||
Le pivot est **supprimé franchement** — pas de feature flag, pas de route cachée. Git conserve l'historique si on veut le ressusciter un jour. Concrètement :
|
||
|
||
- Delete `src/components/reports/DynamicReport.tsx`, `DynamicReportPanel.tsx`, `DynamicReportTable.tsx`, `DynamicReportChart.tsx`
|
||
- Retirer `pivotConfig`, `pivotResult`, les actions `setPivotConfig` et la logique `tab === 'dynamic'` de `src/hooks/useReports.ts`
|
||
- Retirer toutes les clés `reports.pivot.*` dans `src/i18n/locales/{fr,en}.json`
|
||
- Nettoyer le type `ReportTab` (plus de `'dynamic'`)
|
||
|
||
**Sidebar (`NAV_ITEMS` dans `src/shared/constants/index.ts`)** : l'entrée `/reports` reste seule — les 4 sous-rapports ne sont accessibles que via les cartes du hub.
|
||
|
||
> **🔴 TECHNIQUE + ARCHITECTURE** — `src/shared/constants.ts` n'existe pas + feature flag probablement YAGNI.
|
||
> Le projet utilise `src/shared/constants/index.ts` (dossier avec barrel). En plus, un flag pour du code déjà retiré de l'UI = route jamais atteinte + dette i18n (`reports.pivot.*` conservées) sans bénéfice clair — git garde l'historique.
|
||
> **Resolution :** Soit supprimer franchement les `DynamicReport*` et leurs clés i18n (git = historique), soit ajouter le flag dans `src/shared/constants/index.ts` avec un TODO de suppression à 2 versions. Trancher maintenant et écrire le choix dans la spec.
|
||
|
||
> **🟢 SECURITE** — Pivot flag runtime laisse le code dans le bundle.
|
||
> Un constant JS ne tree-shake pas : `getDynamicReportData` et son `FIELD_SQL` dynamique (dont les filtres viennent de l'utilisateur) restent dans le JS shippé = surface d'attaque morte mais live.
|
||
> **Resolution :** Si on garde l'option, utiliser un flag build-time via `import.meta.env.VITE_ENABLE_LEGACY_PIVOT` ou un `define` Vite pour permettre au bundler d'éliminer l'import conditionnel.
|
||
> *Ref : OWASP A05:2021*
|
||
|
||
### i18n
|
||
|
||
Nouveaux espaces dans `src/i18n/locales/{fr,en}.json` :
|
||
|
||
```json
|
||
"reports": {
|
||
"hub": {
|
||
"title": "Rapports",
|
||
"explore": "Explorer",
|
||
"highlights": "Faits saillants",
|
||
"trends": "Tendances",
|
||
"compare": "Comparables",
|
||
"categoryZoom": "Analyse par catégorie"
|
||
},
|
||
"highlights": {
|
||
"netBalanceCurrent": "Solde du mois",
|
||
"netBalanceYtd": "Solde cumulatif (YTD)",
|
||
"topMovers": "Top mouvements",
|
||
"topTransactions": "Plus grosses transactions récentes",
|
||
"variationAbs": "Écart $",
|
||
"variationPct": "Écart %",
|
||
"vsLastMonth": "vs mois précédent"
|
||
},
|
||
"trends": {
|
||
"subviewGlobal": "Flux global",
|
||
"subviewByCategory": "Par catégorie"
|
||
},
|
||
"compare": {
|
||
"modeMoM": "Mois vs mois précédent",
|
||
"modeYoY": "Année vs année précédente",
|
||
"modeBudget": "Réel vs budget",
|
||
"delta": "Écart",
|
||
"current": "Courant",
|
||
"previous": "Précédent"
|
||
},
|
||
"category": {
|
||
"selectCategory": "Choisir une catégorie",
|
||
"includeSubcategories": "Inclure sous-catégories",
|
||
"directOnly": "Directe seulement",
|
||
"breakdown": "Répartition",
|
||
"evolution": "Évolution",
|
||
"transactions": "Transactions"
|
||
},
|
||
"viewMode": {
|
||
"chart": "Graphique",
|
||
"table": "Tableau"
|
||
},
|
||
"keyword": {
|
||
"addFromTransaction": "Ajouter comme mot-clé",
|
||
"dialogTitle": "Nouveau mot-clé",
|
||
"willMatch": "Matchera aussi",
|
||
"nMatches_one": "{{count}} transaction matchée",
|
||
"nMatches_other": "{{count}} transactions matchées",
|
||
"applyAndRecategorize": "Appliquer et recatégoriser",
|
||
"tooShort": "Minimum 2 caractères",
|
||
"tooLong": "Maximum 64 caractères",
|
||
"alreadyExists": "Ce mot-clé existe déjà pour la catégorie « {{category}} ». Remplacer ?"
|
||
},
|
||
"empty": {
|
||
"noData": "Aucune donnée pour cette période",
|
||
"importCta": "Importer un relevé"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Suffixes de pluriel** : i18next v25 + react-i18next v16 exige le format v4 JSON (`_one` / `_other`), pas `_plural`. Les clés `reports.pivot.*` existantes sont **supprimées** avec le code du pivot (pas conservées).
|
||
|
||
> **🔴 TECHNIQUE** — Suffixe `_plural` incorrect pour i18next v25.
|
||
> Le projet tourne avec `i18next` v25 + `react-i18next` v16 qui exige le format v4 JSON (`_one` / `_other`). Les clés existantes utilisent déjà `fileCount_one` / `fileCount_other`. `nMatches_plural` ne résoudra jamais, silencieusement.
|
||
> **Resolution :** Remplacer `nMatches` / `nMatches_plural` par `nMatches_one` / `nMatches_other` dans le snippet i18n et dans la task d'Issue 5.
|
||
|
||
## Plan de travail
|
||
|
||
Découpage en **8 issues Forgejo** (milestone `spec-refonte-rapports`). Les tâches détaillées vivent dans chaque issue Forgejo — cette section en donne l'index.
|
||
|
||
### Issue #1 (1a) — Fondation non-breaking [type:task]
|
||
**Dépendances :** aucune
|
||
|
||
Supprime le pivot franchement, ajoute les 4 squelettes de pages, crée les shared components (`ViewModeToggle`, `Sparkline`, `ContextMenu` générique), ajoute les sous-routes, met à jour les clés i18n. N'introduit pas encore le hub ni la refonte `useReports` — rien n'est cassé côté rapports existants.
|
||
|
||
### Issue #2 (1b) — Refonte `useReports` en hooks par domaine + query string période [type:task]
|
||
**Dépendances :** #1
|
||
|
||
Crée `useReportsPeriod` (query string), `useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`. Garde `useReports` en mode legacy temporaire pour que les 4 rapports existants continuent de tourner pendant la transition.
|
||
|
||
### Issue #3 — Rapport Faits saillants + Hub [type:feature]
|
||
**Dépendances :** #2
|
||
|
||
Implémente `getHighlights` et les tuiles (`HubNetBalanceTile`, `HubTopMoversTile`, `HubTopTransactionsTile`), compose `HubHighlightsPanel`, transforme `/reports` en hub (grille de 4 `HubReportNavCard`), implémente la version détaillée `ReportsHighlightsPage`.
|
||
|
||
### Issue #4 — Rapport Tendances [type:feature]
|
||
**Dépendances :** #2
|
||
|
||
`ReportsTrendsPage` avec sous-toggle `Flux global` / `Par catégorie`. Réutilise `MonthlyTrendsChart/Table` et `CategoryOverTimeChart/Table` existants, les recâble sur `useTrends` + `useReportsPeriod`.
|
||
|
||
### Issue #5 — Rapport Comparables [type:feature]
|
||
**Dépendances :** #2
|
||
|
||
`getCompareMonthOverMonth`, `getCompareYearOverYear` (SQL paramétré). `ReportsComparePage` avec `CompareModeTabs` (MoM / YoY / Budget), `ComparePeriodTable`, `ComparePeriodChart` (diverging bar), `CompareBudgetView` (wrap de `BudgetVsActualTable`).
|
||
|
||
### Issue #6 — Zoom catégorie + édition contextuelle mot-clé (scope limité) [type:feature]
|
||
**Dépendances :** #2
|
||
|
||
`getCategoryZoom` avec **CTE récursive bornée** (cycle guard). `ReportsCategoryPage` avec combobox + rollup, `CategoryDonutChart` (template `dashboard/CategoryPieChart.tsx`), `CategoryEvolutionChart`, `CategoryTransactionsTable`.
|
||
|
||
Édition contextuelle des mots-clés : exporte `normalizeDescription`, `buildKeywordRegex`, `compileKeywords` depuis `categorizationService.ts` ; crée `src/components/categories/AddKeywordDialog.tsx` avec toutes les contraintes de sécurité (SQL paramétré, validation longueur 2–64, transaction SQL englobante, apply uniquement aux cochées visibles, rendu XSS-safe). Branche `ContextMenu` **uniquement sur `CategoryTransactionsTable`** dans cette issue. Test unitaire cycle guard avec fixture cyclique.
|
||
|
||
### Issue #7 — Propagation du clic droit (follow-up) [type:feature]
|
||
**Dépendances :** #3, #4, #5, #6
|
||
|
||
Étend `ContextMenu` + `AddKeywordDialog` aux autres tables : `HighlightsTopMoversTable`, `HighlightsTopTransactionsList`, `ComparePeriodTable`, `MonthlyTrendsTable`, `CategoryOverTimeTable`, et la table principale de `TransactionsPage`. Pas de nouveau code métier — réutilisation pure.
|
||
|
||
### Issue #8 — Polish, tests, documentation, changelog [type:task]
|
||
**Dépendances :** #3, #4, #5, #6, #7
|
||
|
||
Smoke tests vitest (`getHighlights`, `getCompareMonthOverMonth`, `getCategoryZoom` avec fixture cyclique, validation longueur `AddKeywordDialog`). Validation manuelle des flows. Mise à jour `docs/architecture.md` + `docs/guide-utilisateur.md` + ADR. Entrées `CHANGELOG.md` / `CHANGELOG.fr.md` sous `## [Unreleased]`. Suppression définitive des champs legacy de `useReports`. Build + tests verts.
|
||
|
||
### Ordre d'exécution
|
||
|
||
```
|
||
#1 → #2 → #3 ─┐
|
||
→ #4 ─┤
|
||
→ #5 ─┼→ #7 → #8
|
||
→ #6 ─┘
|
||
```
|
||
|
||
Issues #3, #4, #5, #6 parallélisables après #2.
|
||
|
||
## Fichiers concernés
|
||
|
||
| Fichier | Action | Raison |
|
||
|---|---|---|
|
||
| `src/pages/ReportsPage.tsx` | Refondre | Devient le hub (#3) |
|
||
| `src/pages/ReportsHighlightsPage.tsx` | Créer | Sous-page plat (#1 skeleton, #3 contenu) |
|
||
| `src/pages/ReportsTrendsPage.tsx` | Créer | Sous-page (#1 skeleton, #4 contenu) |
|
||
| `src/pages/ReportsComparePage.tsx` | Créer | Sous-page (#1 skeleton, #5 contenu) |
|
||
| `src/pages/ReportsCategoryPage.tsx` | Créer | Sous-page (#1 skeleton, #6 contenu) |
|
||
| `src/App.tsx` | Modifier | Ajout des 4 sous-routes (#1) |
|
||
| `src/hooks/useReports.ts` | Refondre → supprimer | Nettoyage pivot (#1), déprécié (#2), supprimé (#8) |
|
||
| `src/hooks/useReportsPeriod.ts` | Créer | Période via query string (#2) |
|
||
| `src/hooks/useHighlights.ts` | Créer | Hook domaine (#2) |
|
||
| `src/hooks/useTrends.ts` | Créer | Hook domaine (#2) |
|
||
| `src/hooks/useCompare.ts` | Créer | Hook domaine (#2) |
|
||
| `src/hooks/useCategoryZoom.ts` | Créer | Hook domaine (#2) |
|
||
| `src/services/reportService.ts` | Étendre | `getHighlights` (#3), `getCompareMoM`/`YoY` (#5), `getCategoryZoom` CTE bornée (#6) |
|
||
| `src/services/categorizationService.ts` | Étendre | Exporter `normalizeDescription`, `buildKeywordRegex`, `compileKeywords` (#6) |
|
||
| `src/components/reports/*` (plat, préfixes) | Créer | `Hub*`, `Highlights*`, `Compare*`, `Category*`, `ViewModeToggle`, `Sparkline` |
|
||
| `src/components/reports/DynamicReport*.tsx` | Supprimer | Pivot supprimé franchement (#1) |
|
||
| `src/components/shared/ContextMenu.tsx` | Créer | Shell générique clic droit (#1) |
|
||
| `src/components/shared/ChartContextMenu.tsx` | Refactorer | Compose `ContextMenu` (#1) |
|
||
| `src/components/categories/AddKeywordDialog.tsx` | Créer | Dialog édition mot-clé avec garanties sécurité (#6) |
|
||
| `src/shared/constants/index.ts` | Aucun changement NAV_ITEMS | `/reports` reste seul point d'entrée |
|
||
| `src/i18n/locales/fr.json` + `en.json` | Étendre + nettoyer | Ajouter clés hub/highlights/trends/compare/category/keyword/empty/viewMode ; **supprimer** `reports.pivot.*` (#1) |
|
||
| `CHANGELOG.md` + `CHANGELOG.fr.md` | Modifier | Entrée `## [Unreleased]` (#8) |
|
||
| `docs/architecture.md` | Modifier | Section Rapports mise à jour (#8) |
|
||
| `docs/guide-utilisateur.md` | Modifier | Nouveau flow utilisateur (#8) |
|
||
| `docs/adr/NNNN-refonte-rapports.md` | Créer | Décision architecturale (#8) |
|
||
|
||
## Critères d'acceptation
|
||
|
||
- [ ] La page `/reports` affiche un hub avec un panneau Faits saillants en haut et 4 cartes de navigation.
|
||
- [ ] Chacune des 4 sous-pages (`/reports/highlights`, `/trends`, `/compare`, `/category`) est accessible et fonctionnelle.
|
||
- [ ] Le toggle graphique/tableau fonctionne sur toutes les sous-pages et la préférence est mémorisée par rapport.
|
||
- [ ] Le rapport faits saillants affiche solde mois courant, solde YTD (avec sparklines), top movers par catégorie et top transactions récentes.
|
||
- [ ] Le rapport tendances expose flux global et par catégorie via un toggle interne.
|
||
- [ ] Le rapport comparables permet de basculer entre MoM, YoY et Réel vs budget en conservant la période.
|
||
- [ ] Le rapport zoom catégorie inclut automatiquement les sous-catégories et offre un toggle pour se limiter aux transactions directes.
|
||
- [ ] Le donut chart de répartition s'affiche avec les couleurs des catégories et les patterns SVG.
|
||
- [ ] Clic droit sur une transaction ouvre un menu permettant d'ajouter un mot-clé.
|
||
- [ ] Le dialog d'ajout de mot-clé montre la preview des transactions qui seront recatégorisées, avec cases à cocher individuelles.
|
||
- [ ] Appliquer le mot-clé met à jour `keywords` + les transactions cochées dans une **transaction SQL englobante** (BEGIN/COMMIT/ROLLBACK).
|
||
- [ ] La validation de longueur du mot-clé (2–64 caractères, rejet whitespace-only) est active côté dialog.
|
||
- [ ] `getCategoryZoom` termine correctement avec une fixture de catégories cyclique (test unitaire).
|
||
- [ ] Le tableau croisé dynamique est complètement supprimé du code et des traductions.
|
||
- [ ] Les 4 rapports respectent la signature visuelle : palette couleurs catégorie + patterns SVG + Recharts.
|
||
- [ ] Toutes les chaînes sont traduites en FR et EN.
|
||
- [ ] `CHANGELOG.md` et `CHANGELOG.fr.md` sont mis à jour sous `## [Unreleased]`.
|
||
- [ ] `npm run build` et `cargo check` passent verts.
|
||
- [ ] Un smoke test vitest couvre `getHighlights` et un rapport comparables.
|
||
|
||
## Edge cases et risques
|
||
|
||
| Cas | Mitigation |
|
||
|---|---|
|
||
| Profil vide (aucune transaction) | Chaque rapport affiche un empty-state i18n avec CTA vers /import |
|
||
| Catégorie sans sous-catégories mais rollup activé | Le donut affiche uniquement la catégorie elle-même, pas de plantage |
|
||
| Transaction sans catégorie dans top transactions | Afficher "Non catégorisé" avec couleur `#9ca3af` existante |
|
||
| Mot-clé trop court / trop long / whitespace-only | Validation dialog : longueur 2–64 après `.trim()`, rejet avec i18n `reports.keyword.tooShort` / `tooLong` avant INSERT (prévient ReDoS) |
|
||
| Mot-clé qui matcherait des centaines de transactions | Dialog limite l'affichage à 50 matches. Si plus, checkbox explicite « Appliquer aussi aux N-50 non affichées » (off par défaut). Apply ne touche QUE les lignes cochées réellement visibles, sauf confirmation explicite |
|
||
| Mot-clé avec regex spéciale (ex : `*`, `?`) | `buildKeywordRegex` échappe déjà les metacharactères — couvert par test |
|
||
| Clic droit sur transaction déjà dans la catégorie cible | Le dialog propose juste d'ajouter le mot-clé, sans UPDATE inutile ; afficher "déjà classée" |
|
||
| Conflit de mot-clé (déjà existant pour autre catégorie) | Dialog affiche `reports.keyword.alreadyExists`. « Remplacer » = `UPDATE keywords SET category_id=? WHERE keyword=?` + re-run catégorisation **uniquement** sur matches visibles cochés (pas rétroactif sur l'historique) |
|
||
| Crash au milieu de l'apply (INSERT ok, UPDATE partiel) | Tout l'apply tourne dans une transaction SQL (BEGIN/COMMIT/ROLLBACK). En cas d'échec, rollback complet + toast erreur |
|
||
| Catégorie avec cycle dans `parent_id` (A→B→A) | `getCategoryZoom` utilise une CTE récursive bornée `WHERE depth < 5`. Test unitaire avec fixture cyclique |
|
||
| Year-over-year sans données année précédente | Afficher empty-state i18n `reports.empty.noData` |
|
||
| Budget absent dans Réel vs budget | Réutiliser le comportement existant de `BudgetVsActualTable` |
|
||
| Profil existant avec `tab: 'dynamic'` en localStorage | Fallback sur hub à l'ouverture — le pivot n'existe plus |
|
||
| Période personnalisée très longue (5+ ans) | Laisser passer, Recharts gère bien jusqu'à ~60 points |
|
||
| XSS via description de transaction importée | Descriptions rendues comme enfants React uniquement (jamais `dangerouslySetInnerHTML`), troncature CSS |
|
||
|
||
## Décisions prises
|
||
|
||
| Question | Décision | Raison |
|
||
|---|---|---|
|
||
| Organisation UI | Hub + 4 sous-pages | Offre un récit (faits saillants d'abord) + place pour chaque rapport |
|
||
| Contenu faits saillants | Top mouvements + top transactions + solde mois/YTD | Répond à la question "qu'est-ce qui a bougé ?" sans complexifier |
|
||
| Contenu comparables | MoM, YoY, Réel vs budget avec navigation facile | Couvre les 3 comparaisons naturelles (temps court, temps long, vs plan) |
|
||
| Analyse ponctuelle | Zoom sur catégorie (pas pivot) | 90% des usages du pivot revenaient à zoomer une catégorie |
|
||
| Sort du pivot | **Supprimé franchement** (code + i18n + types) | Git conserve l'historique. Feature flag runtime laisserait le code dans le bundle (surface d'attaque morte) + dette i18n inutile. YAGNI |
|
||
| Contenu tendances | Flux global + par catégorie uniquement | Retire projection et moyennes mobiles (hors scope) |
|
||
| Toggle chart/table | Partout, défaut graphique, mémorisé localStorage | Cohérence + flexibilité + mémoire utilisateur |
|
||
| Librairie de charts | Conserver Recharts + patterns SVG | Déjà en place, adapté aux petits datasets, signature visuelle intacte |
|
||
| Nouveaux types | Sparklines + donut chart | Utiles pour faits saillants et répartition, faisables en Recharts natif |
|
||
| Rollup sous-catégories | Auto activé, toggle pour désactiver | Intuition conforme à l'arbre, reste contrôlable |
|
||
| Édition mots-clés | Clic droit contextuel sur transaction | Contexte naturel, pas de duplication d'UI dans categories |
|
||
| Reclassification | Preview + apply | Sécurité + feedback + contrôle utilisateur |
|
||
| Période par défaut | Année civile en cours | Naturel fiscalement, cohérent avec budget |
|
||
| Découpage issues | **8 issues** : #1 fondation non-breaking, #2 refonte hooks, #3–6 rapports (parallélisables), #7 propagation clic droit, #8 polish | Split fondation en 1a+1b évite de casser les rapports existants pendant la refonte du hook. Issue #7 de follow-up évite que Issue #6 dépende de #3/#4/#5 |
|
||
| Partage de période entre routes | Query string `?from=YYYY-MM-DD&to=YYYY-MM-DD` via `useSearchParams` | Bookmarkable, pas de contexte React global, cohérent avec le reste du projet |
|
||
| Hook `useReports` | Split en hooks par domaine (`useReportsPeriod`, `useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`) | Chaque route monte uniquement son hook, pas de god-object, pas de refetch de champs hors-section |
|
||
| Structure `src/pages/` | Plate (pas de sous-dossier) | Cohérent avec le reste du projet (`DashboardPage`, `ImportPage`, etc.) |
|
||
| Structure `src/components/reports/` | Plate avec préfixes de nom (`HubNetBalanceTile`, etc.) | Cohérent avec les autres dossiers composants (`import/`, `dashboard/`, `profile/`) qui sont plats |
|
||
| `AddKeywordDialog` emplacement | `src/components/categories/` | Domaine édition mot-clé, utilisé hors reports (page transactions aussi) |
|
||
| `ContextMenu` générique | Créer `src/components/shared/ContextMenu.tsx` + refactorer `ChartContextMenu` pour le composer | Évite de dupliquer la logique click-outside + Escape déjà dans `ChartContextMenu` |
|
||
| Sécurité du dialog mot-clé | SQL paramétré + longueur 2–64 + transaction SQL + apply sur cochées visibles + rendu React children | 3 critiques sécurité (SQL injection, ReDoS, transaction atomique) + 1 XSS latent dans le webview Tauri |
|
||
| Cycle guard rollup catégorie | CTE SQLite récursive bornée `WHERE depth < 5` + test fixture cyclique | `categories.parent_id` ne protège pas contre les cycles, risque de boucle infinie |
|
||
|
||
## Références
|
||
|
||
| Source | Pertinence |
|
||
|---|---|
|
||
| [My Take on React Chart Libraries — Kyle Gill](https://www.kylegill.com/essays/react-chart-libraries) | Confirme que Recharts reste un choix solide pour React + petits datasets ; pas de raison de migrer |
|
||
| [Top React Chart Libraries 2026 — Querio](https://querio.ai/articles/top-react-chart-libraries-data-visualization) | Compare Recharts / Visx / ECharts ; ECharts pertinent uniquement >100k points, pas notre cas |
|
||
| [Recharts — Create a Donut Chart (GeeksforGeeks)](https://www.geeksforgeeks.org/reactjs/create-a-donut-chart-using-recharts-in-reactjs/) | Donut chart = `Pie` avec `innerRadius`, pas de dépendance supplémentaire |
|
||
| [MUI X — Sparkline Chart](https://mui.com/x/react-charts/sparkline/) | Pattern sparkline : LineChart compact sans axes, intégrable dans une tuile |
|
||
|
||
## Revision — Synthese
|
||
|
||
> Date : 2026-04-13 | Experts : Securite, Architecture, Technique
|
||
|
||
### Verdict
|
||
|
||
🔴 **CRITIQUES A CORRIGER** — L'orientation produit (hub + 4 rapports) est validée, mais 9 findings critiques doivent être résolus avant d'ouvrir les issues : incohérences avec la structure du projet, références à du code inexistant, et risques de sécurité autour de l'édition contextuelle des mots-clés.
|
||
|
||
### Resume
|
||
|
||
| Expert | 🔴 | 🟡 | 🟢 | Points cles |
|
||
|--------|----|----|----|-------------|
|
||
| Securite | 3 | 3 | 2 | ReDoS + SQL injection sur dialog mot-clé, absence de cycle guard rollup catégorie, transaction SQL manquante |
|
||
| Architecture | 3 | 3 | 1 | `pages/reports/` casse la convention flat, `useReports` god-object, partage de période non spécifié |
|
||
| Technique | 3 | 3 | 2 | `normalizeString` inexistant, `constants.ts` mauvais path, i18next v25 exige `_one`/`_other` |
|
||
|
||
### Actions requises
|
||
|
||
**🔴 Critiques à corriger avant d'ouvrir les issues**
|
||
|
||
1. 🔴 **Preview SQL doit être paramétrée** — charger les candidats via `LIKE ?` puis filtrer en mémoire avec `buildKeywordRegex` ; jamais de string concat.
|
||
2. 🔴 **ReDoS : cap la longueur du mot-clé** — validation 2–64 caractères dans `AddKeywordDialog`, rejet whitespace-only.
|
||
3. 🔴 **Cycle guard sur le rollup catégorie** — CTE récursive bornée (`WHERE depth < 5`) ou `Set<visited>` en JS + test unitaire.
|
||
4. 🔴 **Structure `src/pages/` plate** — renommer `ReportsHighlightsPage.tsx`, `ReportsTrendsPage.tsx`, etc. à côté de `ReportsPage.tsx`, pas de sous-dossier.
|
||
5. 🔴 **Splitter `useReports`** — un hook par domaine (`useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`) + `useReportsPeriod` partagé via query string.
|
||
6. 🔴 **Mécanisme de partage de période** — query string `?from=...&to=...`, pas de contexte React global.
|
||
7. 🔴 **`nMatches_one` / `nMatches_other`** — pas `_plural` (i18next v25).
|
||
8. 🔴 **Corriger `src/shared/constants/index.ts`** — ou mieux : supprimer franchement le pivot, git garde l'historique (trancher YAGNI).
|
||
9. 🔴 **Exporter `normalizeDescription` et `buildKeywordRegex`** — et corriger le nom dans la spec (pas `normalizeString`).
|
||
|
||
**🟡 Améliorations recommandées**
|
||
|
||
10. 🟡 Wrapper INSERT + UPDATE du dialog mot-clé dans une transaction SQL explicite.
|
||
11. 🟡 Decider et écrire le comportement de « remplacer un mot-clé existant ».
|
||
12. 🟡 Apply ne modifie que les lignes cochées affichées (ou confirmation explicite pour les N-50 non affichées).
|
||
13. 🟡 Garder `components/reports/` plat avec préfixes de nom (`HubNetBalanceTile`, etc.), comme les autres dossiers composants.
|
||
14. 🟡 Déplacer `AddKeywordDialog` → `components/categories/`, `TransactionContextMenu` → `components/shared/`.
|
||
15. 🟡 Splitter Issue 1 en 1a (routing + skeletons) et 1b (refactor `useReports`) pour éviter de casser les 4 rapports existants.
|
||
16. 🟡 Trancher l'exposition Sidebar des sous-routes (probablement : seule `/reports` reste dans le menu).
|
||
17. 🟡 Généraliser `ChartContextMenu` existant plutôt que dupliquer en `TransactionContextMenu`.
|
||
18. 🟡 Scope du clic droit dans Issue 5 : limiter à `CategoryTransactionsTable` + issue de follow-up pour propagation.
|