From 8d5fab966a5f4e8f08065081dd25014e7ad5f996 Mon Sep 17 00:00:00 2001 From: le king fu Date: Tue, 14 Apr 2026 15:29:49 -0400 Subject: [PATCH] docs: polish + changelog + ADR + legacy cleanup for reports refactor (#76) - Delete legacy src/hooks/useReports.ts (the monolithic hook is now fully replaced by the per-domain hooks from #70) - Delete src/components/reports/ReportFilterPanel.tsx (last caller was the pre-refactor ReportsPage; no longer referenced anywhere) - Update docs/architecture.md: reports hook list now lists the 5 per-domain hooks, reports service entry lists every new endpoint, routing section lists the 4 sub-routes, categorizationService entry mentions the new keyword-editing helpers, components folder count + page count updated - Update docs/guide-utilisateur.md section 9: rewrite around hub + 4 sub-reports, explain bookmarkable period via query string, walk through the right-click keyword editing flow, remove stale pivot section - Rewrite in-app docs.reports.* i18n in both FR and EN to match the new UX (hub, sub-reports, contextual keywords) - New ADR docs/adr/0007-reports-hub-refactor.md: context, decision (hub + four routes, per-domain hooks, URL period, security guarantees on the keyword dialog, bounded recursive CTE for category zoom), consequences, alternatives considered - CHANGELOG.md + CHANGELOG.fr.md: Unreleased entries describing the hub, each sub-report, contextual keyword editing, bookmarkable period, view mode persistence, useReports split, pivot removal, and the security posture of AddKeywordDialog / getCategoryZoom Fixes #76 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.fr.md | 21 +++ CHANGELOG.md | 21 +++ docs/adr/0007-reports-hub-refactor.md | 95 ++++++++++ docs/architecture.md | 22 ++- docs/guide-utilisateur.md | 81 ++++---- src/components/reports/ReportFilterPanel.tsx | 166 ----------------- src/hooks/useReports.ts | 186 ------------------- src/i18n/locales/en.json | 36 ++-- src/i18n/locales/fr.json | 36 ++-- 9 files changed, 229 insertions(+), 435 deletions(-) create mode 100644 docs/adr/0007-reports-hub-refactor.md delete mode 100644 src/components/reports/ReportFilterPanel.tsx delete mode 100644 src/hooks/useReports.ts diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index db51e53..5034bb0 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,27 @@ ## [Non publié] +### Ajouté +- **Hub des rapports** : `/reports` devient un hub affichant un panneau de faits saillants (solde mois courant + cumul annuel avec sparklines, top mouvements vs mois précédent, plus grosses transactions récentes) et quatre cartes de navigation vers des sous-rapports dédiés (#69–#76) +- **Rapport Faits saillants** (`/reports/highlights`) : tuiles de solde avec sparklines 12 mois, tableau triable des top mouvements, graphique en barres divergentes, liste des grosses transactions avec fenêtre 30/60/90 jours (#71) +- **Rapport Tendances** (`/reports/trends`) : bascule interne entre flux global (revenus vs dépenses) et évolution par catégorie, toggle graphique/tableau sur les deux (#72) +- **Rapport Comparables** (`/reports/compare`) : barre d'onglets pour Mois vs Mois précédent, Année vs Année précédente, et Réel vs Budget ; graphique en barres divergentes centré sur zéro pour les deux premiers modes (#73) +- **Zoom catégorie** (`/reports/category`) : analyse ciblée avec donut chart de la répartition par sous-catégorie, graphique d'évolution mensuelle en aires, et tableau filtrable des transactions (#74) +- **Édition contextuelle des mots-clés** : clic droit sur n'importe quelle ligne de transaction pour ajouter sa description comme mot-clé de catégorisation ; un dialog de prévisualisation montre toutes les transactions qui seraient recatégorisées (limitées à 50, avec checkbox explicite pour les suivantes) avant validation. Disponible sur le zoom catégorie, la liste des faits saillants, et la page Transactions principale (#74, #75) +- **Période bookmarkable** : la période des rapports vit maintenant dans l'URL (`?from=YYYY-MM-DD&to=YYYY-MM-DD`), vous pouvez copier, coller et partager le lien en conservant l'état (#70) +- **Préférence chart/table** mémorisée dans `localStorage` par section de rapport + +### Modifié +- Le hook monolithique `useReports` a été splitté en hooks par domaine (`useReportsPeriod`, `useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`) pour que chaque sous-rapport ne possède que l'état qu'il utilise (#70) +- Le menu contextuel (clic droit) des rapports est désormais un composant générique `ContextMenu` réutilisé par le menu des graphiques existant et le nouveau dialog d'ajout de mot-clé (#69) + +### Supprimé +- Le tableau croisé dynamique a été retiré. Plus de 90 % de son usage réel consistait à zoomer sur une catégorie, ce que le nouveau rapport Zoom catégorie traite mieux. L'historique git préserve l'ancienne implémentation si jamais elle doit revenir (#69) + +### Sécurité +- Le nouveau `AddKeywordDialog` impose une longueur de 2 à 64 caractères sur les mots-clés utilisateurs pour empêcher les attaques ReDoS sur de grands ensembles de transactions (CWE-1333), utilise des requêtes `LIKE` paramétrées pour la prévisualisation (CWE-89), encapsule l'INSERT + les UPDATE par transaction dans une transaction SQL BEGIN/COMMIT/ROLLBACK explicite (CWE-662), affiche toutes les descriptions non-sûres via rendu React enfants (CWE-79), et ne recatégorise que les lignes explicitement cochées par l'utilisateur — jamais rétroactivement. Le remplacement d'un mot-clé existant sur une autre catégorie nécessite une confirmation explicite (#74) +- `getCategoryZoom` parcourt l'arbre des catégories via une CTE récursive **bornée** (`WHERE depth < 5`), protégeant contre les cycles `parent_id` malformés (CWE-835) (#74) + ## [0.7.4] - 2026-04-14 ### Modifié diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce8083..eae7947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ ## [Unreleased] +### Added +- **Reports hub**: `/reports` is now a hub surfacing a live highlights panel (current month + YTD net balance with sparklines, top movers vs. last month, top recent transactions) and four navigation cards to dedicated sub-reports (#69–#76) +- **Highlights report** (`/reports/highlights`): balance tiles with 12-month sparklines, sortable top movers table, diverging bar chart, recent transactions list with 30/60/90 day window toggle (#71) +- **Trends report** (`/reports/trends`): internal sub-view toggle between global flow (income vs. expenses) and by-category evolution, chart/table toggle on both (#72) +- **Compare report** (`/reports/compare`): tab bar for Month vs. Previous Month, Year vs. Previous Year, and Actual vs. Budget; diverging bar chart centered on zero for the first two modes (#73) +- **Category zoom** (`/reports/category`): single-category drill-down with donut chart of subcategory breakdown, monthly evolution area chart, and filterable transactions table (#74) +- **Contextual keyword editing**: right-click any transaction row to add its description as a categorization keyword; a preview dialog shows every transaction that would be recategorized (capped at 50, with an opt-in checkbox for N+) before you confirm. Available on the category zoom, the highlights list, and the main transactions page (#74, #75) +- **Bookmarkable period**: the reports period now lives in the URL (`?from=YYYY-MM-DD&to=YYYY-MM-DD`), so you can copy, paste, and share the link and keep the same state (#70) +- **View mode preference** (chart vs. table) is now persisted in `localStorage` per report section + +### Changed +- The legacy monolithic `useReports` hook has been split into per-domain hooks (`useReportsPeriod`, `useHighlights`, `useTrends`, `useCompare`, `useCategoryZoom`) so every sub-report owns only the state it needs (#70) +- Context menu on reports (right-click) is now a generic `ContextMenu` shell reused by the existing chart menu and the new keyword dialog (#69) + +### Removed +- The dynamic pivot table report was removed. Over 90% of its real usage was zooming into a single category, which is better served by the new Category Zoom report. Git history preserves the old implementation if it ever needs to come back (#69) + +### Security +- New `AddKeywordDialog` enforces a 2–64 character length bound on user keywords to prevent ReDoS on large transaction sets (CWE-1333), uses parameterized `LIKE` queries for the preview (CWE-89), wraps its INSERT + per-transaction UPDATEs in an explicit BEGIN/COMMIT/ROLLBACK transaction (CWE-662), renders all untrusted descriptions as React children (CWE-79), and recategorizes only the rows the user explicitly checked — never retroactively. Keyword reassignment across categories requires an explicit confirmation step (#74) +- `getCategoryZoom` walks the category tree through a **bounded** recursive CTE (`WHERE depth < 5`), protecting against malformed `parent_id` cycles (CWE-835) (#74) + ## [0.7.4] - 2026-04-14 ### Changed diff --git a/docs/adr/0007-reports-hub-refactor.md b/docs/adr/0007-reports-hub-refactor.md new file mode 100644 index 0000000..58031d9 --- /dev/null +++ b/docs/adr/0007-reports-hub-refactor.md @@ -0,0 +1,95 @@ +# ADR 0007 — Reports hub refactor + +- Status: Accepted +- Date: 2026-04-14 +- Milestone: `spec-refonte-rapports` + +## Context + +The original `/reports` page exposed five tabs (`trends`, `byCategory`, `overTime`, `budgetVsActual`, `dynamic`) as independent analytic views backed by a single monolithic `useReports` hook. Three problems built up over time: + +1. **No narrative.** None of the tabs answered "what's important to know about my finances this month?". Users had to navigate several tabs and reconstruct the story themselves. +2. **Oversized pivot.** The dynamic pivot table (`DynamicReport*`) was powerful but complex. In practice ~90 % of its actual usage boiled down to zooming into a single category. It added visual and cognitive debt without proportional value. +3. **Disconnected classification.** Keywords could only be edited from `/categories`. Spotting a mis-classified transaction in a report meant leaving the report, editing a rule, and navigating back — a context break that discouraged hygiene. + +## Decision + +Refactor `/reports` into a **hub + four dedicated sub-routes**, wired to a shared bookmarkable period and per-domain hooks, with contextual keyword editing via right-click. + +### Routing + +``` +/reports → hub (highlights panel + nav cards) +/reports/highlights → detailed highlights +/reports/trends → global flow + by-category evolution +/reports/compare → month vs month / year vs year / actual vs budget +/reports/category → single-category zoom with rollup +``` + +All pages share the reporting period through the URL query string (`?from=YYYY-MM-DD&to=YYYY-MM-DD&period=...`), resolved by a pure `resolveReportsPeriod()` helper. Default: current civil year. The query string approach is deliberately **not** a React context — it keeps the URL bookmarkable and stays consistent with the rest of the project, which does not use global React contexts for UI state. + +### Per-domain hooks + +The monolithic `useReports` was split into: + +| Hook | Responsibility | +|------|----------------| +| `useReportsPeriod` | Read/write period via `useSearchParams` | +| `useHighlights` | Fetch highlights snapshot + window-days state | +| `useTrends` | Fetch global or by-category trends depending on sub-view | +| `useCompare` | Fetch MoM / YoY; budget mode delegates to `BudgetVsActualTable` | +| `useCategoryZoom` | Fetch zoom data with rollup toggle | + +Each page mounts only the hook it needs; no hook carries state for reports the user is not currently viewing. + +### Dynamic pivot removal + +Removed outright rather than hidden behind a feature flag. A runtime flag would leave `getDynamicReportData` and its dynamic `FIELD_SQL` in the shipped bundle as a dead-but-live attack surface (OWASP A05:2021). Git history preserves the previous implementation if it ever needs to come back. + +### Contextual keyword editing + +Right-clicking a transaction row anywhere transaction-level (category zoom, highlights top transactions, main transactions page) opens an `AddKeywordDialog` that: + +1. Validates the keyword is 2–64 characters after trim (anti-ReDoS, CWE-1333). +2. Previews matching transactions via a parameterised `LIKE $1` query, then filters in memory with the existing `buildKeywordRegex` helper (anti-SQL-injection, CWE-89). +3. Caps the visible preview at 50 rows; an explicit opt-in checkbox lets the user extend the apply to N−50 non-displayed matches. +4. Runs INSERT (or UPDATE-reassign) + per-transaction UPDATEs inside a single SQL transaction (`BEGIN`/`COMMIT`/`ROLLBACK`), so a crash mid-apply can never leave a keyword orphaned from its transactions (CWE-662). +5. Renders transaction descriptions as React children — never `dangerouslySetInnerHTML` — with CSS-only truncation (CWE-79). +6. Recategorises only the rows the user explicitly checked; never retroactive on the entire history. + +Reassigning an existing keyword across categories requires an explicit confirmation step and leaves the existing keyword's historical matches alone. + +### Category zoom cycle guard + +`getCategoryZoom` aggregates via a **bounded** recursive CTE (`WITH RECURSIVE ... WHERE ct.depth < 5`) so a corrupted `parent_id` loop (A → B → A) can never spin forever (CWE-835). A unit test with a canned cyclic fixture asserts termination. + +## Consequences + +### Positive + +- Reports now tell a story ("what moved") before offering analytic depth. +- Each sub-route is independently code-splittable and testable. +- Period state is bookmarkable and shareable (copy URL → same view). +- Keyword hygiene happens inside the report, with a preview that's impossible in the old flow. +- The dialog's security guarantees are covered by 13 vitest cases (validation boundaries, parameterised LIKE, regex word-boundary filter, BEGIN/COMMIT wrap, ROLLBACK on failure, reassignment policy). +- The cycle guard is covered by its own test with the depth assertion. + +### Negative / trade-offs + +- Adds five new hooks and ~10 new components. Cognitive surface goes up but each piece is smaller and single-purpose. +- Aggregate tables in the compare and highlights sections intentionally skip the right-click menu (the row represents a category/month, not a transaction, so "add as keyword" is meaningless there). Users looking for consistency may be briefly confused. +- Right-clicking inside the main transactions page now offers two ways to add a keyword: the existing inline Tag button (no preview) and the new contextual dialog (with preview). Documented as complementary — the inline path is for quick manual classification, the dialog for preview-backed rule authoring. + +## Alternatives considered + +- **Keep the five-tab layout and only improve the pivot.** Rejected — it doesn't fix the "no narrative" issue and leaves the oversized pivot problem. +- **Hide the pivot behind a feature flag.** Rejected — the code stays in the bundle, runtime flag cannot be tree-shaken, and the i18n `reports.pivot.*` keys would have to linger indefinitely. Outright removal with git as the escape hatch was cheaper and cleaner. +- **React context for the shared period.** Rejected — the project does not use global React contexts for UI state. Query-string persistence is simpler, bookmarkable, and consistent with the rest of the codebase. +- **A single `ContextMenu` implementation shared across reports and charts.** Chose to generalise the existing `ChartContextMenu` into a `ContextMenu` shell; `ChartContextMenu` now composes the shared shell. Avoids duplicating click-outside + Escape handling. + +## References + +- Spec: `spec-refonte-rapports.md` +- Issues: #69 (foundation), #70 (hooks), #71 (highlights + hub), #72 (trends), #73 (compare), #74 (category zoom + AddKeywordDialog), #75 (right-click propagation), #76 (polish) +- OWASP A03:2021 (injection), A05:2021 (security misconfiguration) +- CWE-79 (XSS), CWE-89 (SQL injection), CWE-662 (improper synchronization), CWE-835 (infinite loop), CWE-1333 (ReDoS) diff --git a/docs/architecture.md b/docs/architecture.md index 71d52b0..755a346 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -34,13 +34,13 @@ simpl-resultat/ │ │ ├── import/ # 13 composants (wizard d'import) │ │ ├── layout/ # AppShell, Sidebar │ │ ├── profile/ # 3 composants (PIN, formulaire, switcher) -│ │ ├── reports/ # 10 composants (graphiques + rapports tabulaires + rapport dynamique) +│ │ ├── reports/ # ~25 composants (hub, faits saillants, tendances, comparables, zoom catégorie) │ │ ├── settings/ # 5 composants (+ LogViewerCard, LicenseCard, AccountCard) │ │ ├── shared/ # 6 composants réutilisables │ │ └── transactions/ # 5 composants │ ├── contexts/ # ProfileContext (état global profil) -│ ├── hooks/ # 14 hooks custom (useReducer) -│ ├── pages/ # 10 pages +│ ├── hooks/ # 18+ hooks custom (useReducer, 5 hooks rapports par domaine) +│ ├── pages/ # 14 pages (dont 4 sous-pages rapports) │ ├── services/ # 14 services métier │ ├── shared/ # Types et constantes partagés │ ├── utils/ # 4 utilitaires (parsing, CSV, charts) @@ -121,11 +121,11 @@ Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le | `importSourceService.ts` | Configuration des sources d'import | | `importedFileService.ts` | Suivi des fichiers importés | | `importConfigTemplateService.ts` | Modèles de configuration d'import | -| `categorizationService.ts` | Catégorisation automatique | +| `categorizationService.ts` | Catégorisation automatique + helpers édition de mot-clé (`validateKeyword`, `previewKeywordMatches`, `applyKeywordWithReassignment`) | | `adjustmentService.ts` | Gestion des ajustements | | `budgetService.ts` | Gestion budgétaire | | `dashboardService.ts` | Agrégation données tableau de bord | -| `reportService.ts` | Génération de rapports et analytique | +| `reportService.ts` | Génération de rapports : `getMonthlyTrends`, `getCategoryOverTime`, `getHighlights`, `getCompareMonthOverMonth`, `getCompareYearOverYear`, `getCategoryZoom` (CTE récursive bornée anti-cycle) | | `dataExportService.ts` | Export de données (chiffré) | | `userPreferenceService.ts` | Stockage préférences utilisateur | | `logService.ts` | Capture des logs console (buffer circulaire, sessionStorage) | @@ -146,7 +146,11 @@ Chaque hook encapsule la logique d'état via `useReducer` : | `useAdjustments` | Ajustements | | `useBudget` | Budget | | `useDashboard` | Métriques du tableau de bord | -| `useReports` | Données analytiques | +| `useReportsPeriod` | Période de reporting synchronisée via query string (bookmarkable) | +| `useHighlights` | Panneau de faits saillants du hub rapports | +| `useTrends` | Rapport Tendances (sous-vue flux global / par catégorie) | +| `useCompare` | Rapport Comparables (mode MoM / YoY / budget) | +| `useCategoryZoom` | Rapport Zoom catégorie avec rollup sous-catégories | | `useDataExport` | Export de données | | `useTheme` | Thème clair/sombre | | `useUpdater` | Mise à jour de l'application (gated par entitlement licence) | @@ -280,7 +284,11 @@ Le routing est défini dans `App.tsx`. Toutes les pages sont englobées par `App | `/categories` | `CategoriesPage` | Gestion hiérarchique | | `/adjustments` | `AdjustmentsPage` | Ajustements manuels | | `/budget` | `BudgetPage` | Planification budgétaire | -| `/reports` | `ReportsPage` | Analytique et rapports | +| `/reports` | `ReportsPage` | Hub des rapports : panneau faits saillants + 4 cartes de navigation | +| `/reports/highlights` | `ReportsHighlightsPage` | Faits saillants détaillés (soldes, top mouvements, top transactions) | +| `/reports/trends` | `ReportsTrendsPage` | Tendances (flux global + par catégorie) | +| `/reports/compare` | `ReportsComparePage` | Comparables (MoM / YoY / Réel vs budget) | +| `/reports/category` | `ReportsCategoryPage` | Zoom catégorie avec rollup + édition contextuelle de mots-clés | | `/settings` | `SettingsPage` | Paramètres | | `/docs` | `DocsPage` | Documentation in-app | | `/changelog` | `ChangelogPage` | Historique des versions (bilingue FR/EN) | diff --git a/docs/guide-utilisateur.md b/docs/guide-utilisateur.md index 4053fee..b1f9e1b 100644 --- a/docs/guide-utilisateur.md +++ b/docs/guide-utilisateur.md @@ -246,51 +246,56 @@ Planifiez votre budget mensuel pour chaque catégorie et suivez le prévu par ra ## 9. Rapports -Visualisez vos données financières avec des graphiques interactifs et comparez votre plan budgétaire au réel. +`/reports` est un **hub** qui répond à quatre questions : *qu'est-ce qui a bougé ce mois ?*, *où je vais sur 12 mois ?*, *comment je me situe vs période précédente ou vs budget ?*, *que se passe-t-il dans cette catégorie ?* -### Fonctionnalités +### Le hub -- Tendances mensuelles : revenus vs dépenses dans le temps (graphique en barres) -- Dépenses par catégorie : répartition des dépenses (graphique circulaire) -- Catégories dans le temps : suivez l'évolution de chaque catégorie (graphique en barres empilées), avec filtre par type (dépense/revenu/transfert) -- Budget vs Réel : tableau comparatif mensuel et cumul annuel -- Rapport dynamique : tableau croisé dynamique (pivot table) personnalisable -- Motifs SVG (lignes, points, hachures) pour distinguer les catégories -- Menu contextuel (clic droit) pour masquer une catégorie ou voir ses transactions -- Détail des transactions par catégorie avec tri par colonne (date, description, montant) -- Toggle pour afficher ou masquer les montants dans le détail des transactions +En haut, un **panneau de faits saillants** condensé : solde net du mois courant + solde cumul annuel (YTD) avec sparkline 12 mois, top mouvements vs mois précédent et top 5 des plus grosses transactions récentes. En bas, quatre cartes mènent aux quatre sous-rapports dédiés. -### Comment faire +Le sélecteur de période en haut à droite est **partagé** entre toutes les pages via l'URL : `?from=YYYY-MM-DD&to=YYYY-MM-DD`. Copiez l'URL pour revenir plus tard au même état ou la partager. -1. Utilisez les onglets pour basculer entre Tendances, Par catégorie, Dans le temps et Budget vs Réel -2. Ajustez la période avec le sélecteur de période -3. Cliquez droit sur une catégorie dans un graphique pour la masquer ou voir le détail de ses transactions -4. Les catégories masquées apparaissent comme pastilles au-dessus du graphique — cliquez dessus pour les réafficher -5. Dans Budget vs Réel, basculez entre les vues Mensuel et Cumul annuel -6. Dans le détail d'une catégorie, cliquez sur un en-tête de colonne pour trier les transactions -7. Utilisez l'icône œil dans le détail pour masquer ou afficher la colonne des montants +### Rapport Faits saillants (`/reports/highlights`) + +- Tuiles de soldes mois courant + YTD avec sparklines 12 mois +- Tableau triable des **top mouvements** (catégories avec la plus forte variation vs mois précédent), ou graphique en barres divergentes centré sur zéro (toggle graphique/tableau) +- Liste des **plus grosses transactions récentes** avec fenêtre configurable 30 / 60 / 90 jours + +### Rapport Tendances (`/reports/trends`) + +- **Flux global** : revenus vs dépenses vs solde net sur la période, en graphique d'aires ou tableau +- **Par catégorie** : évolution de chaque catégorie, en lignes ou tableau pivot + +### Rapport Comparables (`/reports/compare`) + +Trois modes accessibles via un tab bar : + +- **Mois vs mois précédent** — tableau catégories × 2 colonnes + écart $ et % +- **Année vs année précédente** — même logique sur 12 mois vs 12 mois +- **Réel vs budget** — reprend la vue Budget vs Réel avec ses totaux mensuels et cumul annuel + +### Rapport Zoom catégorie (`/reports/category`) + +Choisissez une catégorie dans la combobox en haut. Par défaut le rapport inclut automatiquement les sous-catégories (toggle *Directe seulement* pour les exclure). Vous voyez : + +- Un **donut chart** de la répartition par sous-catégorie avec le total au centre +- Un graphique d'évolution mensuelle de la catégorie +- Un tableau triable des transactions + +### Édition contextuelle des mots-clés + +**Clic droit** sur n'importe quelle transaction (dans le zoom catégorie, la liste des faits saillants, ou la page Transactions) ouvre un menu *Ajouter comme mot-clé*. Un dialog affiche : + +1. Une **prévisualisation** des transactions qui seront recatégorisées (jusqu'à 50 visibles avec cases à cocher individuelles — les matches au-delà de 50 peuvent être appliqués via une case explicite) +2. Un sélecteur de catégorie cible +3. Un bouton **Appliquer et recatégoriser** + +L'application est atomique : soit toutes les transactions cochées sont recatégorisées et le mot-clé enregistré, soit rien n'est fait. Si le mot-clé existait déjà pour une autre catégorie, un prompt vous demande si vous voulez le réassigner — cela ne touche **pas** l'historique, seulement les transactions visibles cochées. ### Astuces -- Les catégories masquées sont mémorisées tant que vous restez sur la page — cliquez sur Tout afficher pour réinitialiser -- Le sélecteur de période s'applique à tous les onglets de graphiques simultanément -- Budget vs Réel affiche l'écart en dollars et en pourcentage pour chaque catégorie -- Les motifs SVG aident les personnes daltoniennes à distinguer les catégories dans les graphiques - -### Rapport dynamique - -Le rapport dynamique fonctionne comme un tableau croisé dynamique (pivot table). Vous composez votre propre rapport en assignant des dimensions et des mesures. - -**Dimensions disponibles :** Année, Mois, Type (dépense/revenu/transfert), Catégorie Niveau 1 (parent), Catégorie Niveau 2 (enfant). - -**Mesures :** Montant périodique (somme), Cumul annuel (YTD). - -1. Cliquez sur un champ disponible dans le panneau de droite -2. Choisissez où le placer : Lignes, Colonnes, Filtres ou Valeurs -3. Le tableau et/ou le graphique se mettent à jour automatiquement -4. Utilisez les filtres pour restreindre les données (ex : Type = dépense uniquement) -5. Basculez entre les vues Tableau, Graphique ou Les deux -6. Cliquez sur le X pour retirer un champ d'une zone +- Le toggle **graphique / tableau** est mémorisé par sous-rapport (vos préférences restent même après redémarrage) +- Les mots-clés doivent faire entre 2 et 64 caractères (protection contre les regex explosives) +- Le zoom catégorie est **protégé contre les arborescences cycliques** : un éventuel `parent_id` malformé ne fait pas planter l'app --- diff --git a/src/components/reports/ReportFilterPanel.tsx b/src/components/reports/ReportFilterPanel.tsx deleted file mode 100644 index 3421185..0000000 --- a/src/components/reports/ReportFilterPanel.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Filter, Search } from "lucide-react"; -import type { ImportSource } from "../../shared/types"; -import type { CategoryTypeFilter } from "../../hooks/useReports"; - -interface ReportFilterPanelProps { - categories: { name: string; color: string }[]; - hiddenCategories: Set; - onToggleHidden: (name: string) => void; - onShowAll: () => void; - sources: ImportSource[]; - selectedSourceId: number | null; - onSourceChange: (id: number | null) => void; - categoryType?: CategoryTypeFilter; - onCategoryTypeChange?: (type: CategoryTypeFilter) => void; -} - -export default function ReportFilterPanel({ - categories, - hiddenCategories, - onToggleHidden, - onShowAll, - sources, - selectedSourceId, - onSourceChange, - categoryType, - onCategoryTypeChange, -}: ReportFilterPanelProps) { - const { t } = useTranslation(); - const [search, setSearch] = useState(""); - const [collapsed, setCollapsed] = useState(false); - - const filtered = search - ? categories.filter((c) => c.name.toLowerCase().includes(search.toLowerCase())) - : categories; - - const allVisible = hiddenCategories.size === 0; - const allHidden = hiddenCategories.size === categories.length; - - return ( -
- {/* Source filter */} - {sources.length > 1 && ( -
-
- - {t("transactions.table.source")} -
-
- -
-
- )} - - {/* Type filter */} - {onCategoryTypeChange && ( -
-
- - {t("categories.type")} -
-
- -
-
- )} - - {/* Category filter */} - {categories.length > 0 &&
- - - {!collapsed && ( -
-
-
- - setSearch(e.target.value)} - placeholder={t("reports.filters.search")} - className="w-full pl-7 pr-2 py-1.5 text-xs rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" - /> -
-
- -
- - -
- -
- {filtered.map((cat) => { - const visible = !hiddenCategories.has(cat.name); - return ( - - ); - })} -
-
- )} -
} -
- ); -} diff --git a/src/hooks/useReports.ts b/src/hooks/useReports.ts deleted file mode 100644 index 47d0718..0000000 --- a/src/hooks/useReports.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * @deprecated — legacy monolithic reports hook. Kept during the refonte - * (Issues #70 → #76) so the pre-existing 4 tabs on `/reports` keep working - * while the new per-domain hooks (useHighlights / useTrends / useCompare / - * useCategoryZoom) are wired up. Will be removed in Issue #76 once every - * report migrates to its own route. - */ -import { useReducer, useCallback, useEffect, useRef } from "react"; -import type { - ReportTab, - MonthlyTrendItem, - CategoryBreakdownItem, - CategoryOverTimeData, - BudgetVsActualRow, -} from "../shared/types"; -import { getMonthlyTrends, getCategoryOverTime } from "../services/reportService"; -import { getExpensesByCategory } from "../services/dashboardService"; -import { getBudgetVsActualData } from "../services/budgetService"; -import { useReportsPeriod } from "./useReportsPeriod"; - -export type CategoryTypeFilter = "expense" | "income" | "transfer" | null; - -interface ReportsState { - tab: ReportTab; - sourceId: number | null; - categoryType: CategoryTypeFilter; - monthlyTrends: MonthlyTrendItem[]; - categorySpending: CategoryBreakdownItem[]; - categoryOverTime: CategoryOverTimeData; - budgetYear: number; - budgetMonth: number; - budgetVsActual: BudgetVsActualRow[]; - isLoading: boolean; - error: string | null; -} - -type ReportsAction = - | { type: "SET_TAB"; payload: ReportTab } - | { type: "SET_LOADING"; payload: boolean } - | { type: "SET_ERROR"; payload: string | null } - | { type: "SET_MONTHLY_TRENDS"; payload: MonthlyTrendItem[] } - | { type: "SET_CATEGORY_SPENDING"; payload: CategoryBreakdownItem[] } - | { type: "SET_CATEGORY_OVER_TIME"; payload: CategoryOverTimeData } - | { type: "SET_BUDGET_MONTH"; payload: { year: number; month: number } } - | { type: "SET_BUDGET_VS_ACTUAL"; payload: BudgetVsActualRow[] } - | { type: "SET_SOURCE_ID"; payload: number | null } - | { type: "SET_CATEGORY_TYPE"; payload: CategoryTypeFilter }; - -const now = new Date(); - -const initialState: ReportsState = { - tab: "trends", - sourceId: null, - categoryType: "expense", - monthlyTrends: [], - categorySpending: [], - categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} }, - budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(), - budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(), - budgetVsActual: [], - isLoading: false, - error: null, -}; - -function reducer(state: ReportsState, action: ReportsAction): ReportsState { - switch (action.type) { - case "SET_TAB": - return { ...state, tab: action.payload }; - case "SET_LOADING": - return { ...state, isLoading: action.payload }; - case "SET_ERROR": - return { ...state, error: action.payload, isLoading: false }; - case "SET_MONTHLY_TRENDS": - return { ...state, monthlyTrends: action.payload, isLoading: false }; - case "SET_CATEGORY_SPENDING": - return { ...state, categorySpending: action.payload, isLoading: false }; - case "SET_CATEGORY_OVER_TIME": - return { ...state, categoryOverTime: action.payload, isLoading: false }; - case "SET_BUDGET_MONTH": - return { ...state, budgetYear: action.payload.year, budgetMonth: action.payload.month }; - case "SET_BUDGET_VS_ACTUAL": - return { ...state, budgetVsActual: action.payload, isLoading: false }; - case "SET_SOURCE_ID": - return { ...state, sourceId: action.payload }; - case "SET_CATEGORY_TYPE": - return { ...state, categoryType: action.payload }; - default: - return state; - } -} - -/** @deprecated — see module-level comment. */ -export function useReports() { - const { from, to, period, setPeriod, setCustomDates } = useReportsPeriod(); - const [innerState, dispatch] = useReducer(reducer, initialState); - const fetchIdRef = useRef(0); - - const fetchData = useCallback( - async ( - tab: ReportTab, - dateFrom: string, - dateTo: string, - budgetYear: number, - budgetMonth: number, - srcId: number | null, - catType: CategoryTypeFilter, - ) => { - const fetchId = ++fetchIdRef.current; - dispatch({ type: "SET_LOADING", payload: true }); - dispatch({ type: "SET_ERROR", payload: null }); - - try { - switch (tab) { - case "trends": { - const data = await getMonthlyTrends(dateFrom, dateTo, srcId ?? undefined); - if (fetchId !== fetchIdRef.current) return; - dispatch({ type: "SET_MONTHLY_TRENDS", payload: data }); - break; - } - case "byCategory": { - const data = await getExpensesByCategory(dateFrom, dateTo, srcId ?? undefined); - if (fetchId !== fetchIdRef.current) return; - dispatch({ type: "SET_CATEGORY_SPENDING", payload: data }); - break; - } - case "overTime": { - const data = await getCategoryOverTime(dateFrom, dateTo, undefined, srcId ?? undefined, catType ?? undefined); - if (fetchId !== fetchIdRef.current) return; - dispatch({ type: "SET_CATEGORY_OVER_TIME", payload: data }); - break; - } - case "budgetVsActual": { - const data = await getBudgetVsActualData(budgetYear, budgetMonth); - if (fetchId !== fetchIdRef.current) return; - dispatch({ type: "SET_BUDGET_VS_ACTUAL", payload: data }); - break; - } - } - } catch (e) { - if (fetchId !== fetchIdRef.current) return; - dispatch({ - type: "SET_ERROR", - payload: e instanceof Error ? e.message : String(e), - }); - } - }, - [], - ); - - useEffect(() => { - fetchData( - innerState.tab, - from, - to, - innerState.budgetYear, - innerState.budgetMonth, - innerState.sourceId, - innerState.categoryType, - ); - }, [fetchData, innerState.tab, from, to, innerState.budgetYear, innerState.budgetMonth, innerState.sourceId, innerState.categoryType]); - - const setTab = useCallback((tab: ReportTab) => { - dispatch({ type: "SET_TAB", payload: tab }); - }, []); - - const setBudgetMonth = useCallback((year: number, month: number) => { - dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } }); - }, []); - - const setSourceId = useCallback((id: number | null) => { - dispatch({ type: "SET_SOURCE_ID", payload: id }); - }, []); - - const setCategoryType = useCallback((catType: CategoryTypeFilter) => { - dispatch({ type: "SET_CATEGORY_TYPE", payload: catType }); - }, []); - - const state = { - ...innerState, - period, - customDateFrom: from, - customDateTo: to, - }; - - return { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setSourceId, setCategoryType }; -} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8fbe755..85d4e5d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -777,31 +777,29 @@ }, "reports": { "title": "Reports", - "overview": "Visualize your financial data with interactive charts and compare your budget plan against actual spending.", + "overview": "A hub with a live highlights panel plus four dedicated sub-reports (Highlights, Trends, Compare, Category Zoom). Every page shares a bookmarkable period via the URL query string.", "features": [ - "Monthly Trends: income vs. expenses over time (bar chart)", - "Expenses by Category: spending breakdown (pie chart)", - "Category Over Time: track how each category evolves (line chart)", - "Budget vs Actual: monthly and year-to-date comparison table", + "Hub: compact highlights panel + 4 navigation cards", + "Highlights: current month and YTD balances with sparklines, top movers vs. last month, top recent transactions (30/60/90 day window)", + "Trends: global flow (income vs. expenses) and by-category evolution with a chart/table toggle", + "Compare: Month vs. Previous Month, Year vs. Previous Year, and Actual vs. Budget", + "Category Zoom: single-category drill-down with donut, monthly evolution, and filterable transaction table; auto-rollup of subcategories", + "Contextual keyword editing: right-click a transaction row to add its description as a keyword with a live preview of the matches", "SVG patterns (lines, dots, crosshatch) to distinguish categories", - "Context menu (right-click) to hide a category or view its transactions", - "Transaction detail by category with sortable columns (date, description, amount)", - "Toggle to show or hide amounts in transaction detail" + "View mode preference (chart vs. table) persisted per report section" ], "steps": [ - "Use the tabs to switch between Trends, By Category, Over Time, and Budget vs Actual views", - "Adjust the time period using the period selector", - "Right-click a category in any chart to hide it or view its transaction details", - "Hidden categories appear as dismissible chips above the chart — click them to show again", - "In Budget vs Actual, toggle between Monthly and Year-to-Date views", - "In the category detail, click a column header to sort transactions", - "Use the eye icon in the detail view to show or hide the amounts column" + "Open /reports to see the highlights panel and four navigation cards", + "Adjust the period with the period selector — it is mirrored in the URL and shared with every sub-report", + "Click a card or a sub-route link to open the corresponding report", + "Toggle chart vs. table on any sub-report — your choice is remembered", + "Right-click any transaction row in the category zoom, highlights list, or transactions page to add a keyword", + "In the keyword dialog, review the preview of matching transactions and confirm to apply" ], "tips": [ - "Hidden categories are remembered while you stay on the page — click Show All to reset", - "The period selector applies to all chart tabs simultaneously", - "Budget vs Actual shows dollar and percentage variance for each category", - "SVG patterns help colorblind users distinguish categories in charts" + "Copy the URL to share a specific period + report state", + "Keywords must be 2–64 characters long", + "The Category Zoom is protected against malformed category trees: a parent_id cycle cannot freeze the app" ] }, "settings": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 53ce475..fe8bd27 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -777,31 +777,29 @@ }, "reports": { "title": "Rapports", - "overview": "Visualisez vos données financières avec des graphiques interactifs et comparez votre plan budgétaire au réel.", + "overview": "Un hub qui affiche un panneau de faits saillants en direct plus quatre sous-rapports dédiés (Faits saillants, Tendances, Comparables, Zoom catégorie). Chaque page partage une période bookmarkable via la query string de l'URL.", "features": [ - "Tendances mensuelles : revenus vs dépenses dans le temps (graphique en barres)", - "Dépenses par catégorie : répartition des dépenses (graphique circulaire)", - "Catégories dans le temps : suivez l'évolution de chaque catégorie (graphique en ligne)", - "Budget vs Réel : tableau comparatif mensuel et cumul annuel", + "Hub : panneau de faits saillants condensé + 4 cartes de navigation", + "Faits saillants : soldes mois courant et cumul annuel avec sparklines, top mouvements vs mois précédent, plus grosses transactions récentes (fenêtre 30/60/90 jours)", + "Tendances : flux global (revenus vs dépenses) et évolution par catégorie avec toggle graphique/tableau", + "Comparables : Mois vs Mois précédent, Année vs Année précédente, et Réel vs Budget", + "Zoom catégorie : analyse d'une seule catégorie avec donut, évolution mensuelle et tableau de transactions filtrable ; rollup automatique des sous-catégories", + "Édition contextuelle des mots-clés : clic droit sur une ligne de transaction pour ajouter sa description comme mot-clé avec prévisualisation en direct des matches", "Motifs SVG (lignes, points, hachures) pour distinguer les catégories", - "Menu contextuel (clic droit) pour masquer une catégorie ou voir ses transactions", - "Détail des transactions par catégorie avec tri par colonne (date, description, montant)", - "Toggle pour afficher ou masquer les montants dans le détail des transactions" + "Préférence chart/table mémorisée par section de rapport" ], "steps": [ - "Utilisez les onglets pour basculer entre Tendances, Par catégorie, Dans le temps et Budget vs Réel", - "Ajustez la période avec le sélecteur de période", - "Cliquez droit sur une catégorie dans un graphique pour la masquer ou voir le détail de ses transactions", - "Les catégories masquées apparaissent comme pastilles au-dessus du graphique — cliquez dessus pour les réafficher", - "Dans Budget vs Réel, basculez entre les vues Mensuel et Cumul annuel", - "Dans le détail d'une catégorie, cliquez sur un en-tête de colonne pour trier les transactions", - "Utilisez l'icône œil dans le détail pour masquer ou afficher la colonne des montants" + "Ouvrez /reports pour voir le panneau de faits saillants et les quatre cartes de navigation", + "Ajustez la période avec le sélecteur — elle est reflétée dans l'URL et partagée avec tous les sous-rapports", + "Cliquez sur une carte ou un lien pour ouvrir le sous-rapport correspondant", + "Basculez graphique/tableau sur n'importe quel sous-rapport — votre choix est mémorisé", + "Cliquez droit sur une ligne de transaction dans le zoom catégorie, la liste des faits saillants, ou la page transactions pour ajouter un mot-clé", + "Dans le dialog de mot-clé, passez en revue la prévisualisation des transactions qui matchent et confirmez pour appliquer" ], "tips": [ - "Les catégories masquées sont mémorisées tant que vous restez sur la page — cliquez sur Tout afficher pour réinitialiser", - "Le sélecteur de période s'applique à tous les onglets de graphiques simultanément", - "Budget vs Réel affiche l'écart en dollars et en pourcentage pour chaque catégorie", - "Les motifs SVG aident les personnes daltoniennes à distinguer les catégories dans les graphiques" + "Copiez l'URL pour partager une période et un rapport spécifiques", + "Les mots-clés doivent faire entre 2 et 64 caractères", + "Le Zoom catégorie est protégé contre les arborescences malformées : un cycle parent_id ne peut pas figer l'app" ] }, "settings": { -- 2.45.2