Compare commits
2 commits
001646db70
...
e93b741f26
| Author | SHA1 | Date | |
|---|---|---|---|
| e93b741f26 | |||
|
|
8d5fab966a |
9 changed files with 229 additions and 435 deletions
|
|
@ -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é
|
||||
|
|
|
|||
21
CHANGELOG.md
21
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
|
||||
|
|
|
|||
95
docs/adr/0007-reports-hub-refactor.md
Normal file
95
docs/adr/0007-reports-hub-refactor.md
Normal file
|
|
@ -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)
|
||||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string>;
|
||||
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 (
|
||||
<div className="w-56 shrink-0 sticky top-4 self-start space-y-3">
|
||||
{/* Source filter */}
|
||||
{sources.length > 1 && (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
<div className="px-3 py-2.5 text-sm font-medium text-[var(--foreground)] flex items-center gap-2">
|
||||
<Filter size={14} className="text-[var(--muted-foreground)]" />
|
||||
{t("transactions.table.source")}
|
||||
</div>
|
||||
<div className="border-t border-[var(--border)] px-2 py-2">
|
||||
<select
|
||||
value={selectedSourceId ?? ""}
|
||||
onChange={(e) => onSourceChange(e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full px-2 py-1.5 text-xs rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
|
||||
>
|
||||
<option value="">{t("transactions.filters.allSources")}</option>
|
||||
{sources.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type filter */}
|
||||
{onCategoryTypeChange && (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
<div className="px-3 py-2.5 text-sm font-medium text-[var(--foreground)] flex items-center gap-2">
|
||||
<Filter size={14} className="text-[var(--muted-foreground)]" />
|
||||
{t("categories.type")}
|
||||
</div>
|
||||
<div className="border-t border-[var(--border)] px-2 py-2">
|
||||
<select
|
||||
value={categoryType ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
const valid: CategoryTypeFilter[] = ["expense", "income", "transfer"];
|
||||
onCategoryTypeChange(valid.includes(v as CategoryTypeFilter) ? (v as CategoryTypeFilter) : null);
|
||||
}}
|
||||
className="w-full px-2 py-1.5 text-xs rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
|
||||
>
|
||||
<option value="">{t("reports.filters.allTypes")}</option>
|
||||
<option value="expense">{t("categories.expense")}</option>
|
||||
<option value="income">{t("categories.income")}</option>
|
||||
<option value="transfer">{t("categories.transfer")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category filter */}
|
||||
{categories.length > 0 && <div className="bg-[var(--card)] border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 text-sm font-medium text-[var(--foreground)] hover:bg-[var(--muted)] transition-colors"
|
||||
>
|
||||
<Filter size={14} className="text-[var(--muted-foreground)]" />
|
||||
{t("reports.filters.title")}
|
||||
<span className="ml-auto text-xs text-[var(--muted-foreground)]">
|
||||
{categories.length - hiddenCategories.size}/{categories.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="border-t border-[var(--border)]">
|
||||
<div className="px-2 py-2">
|
||||
<div className="relative">
|
||||
<Search size={13} className="absolute left-2 top-1/2 -translate-y-1/2 text-[var(--muted-foreground)]" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => 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)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-2 pb-1 flex gap-1">
|
||||
<button
|
||||
onClick={onShowAll}
|
||||
disabled={allVisible}
|
||||
className="text-xs px-2 py-0.5 rounded bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--border)] transition-colors disabled:opacity-40"
|
||||
>
|
||||
{t("reports.filters.all")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => categories.forEach((c) => { if (!hiddenCategories.has(c.name)) onToggleHidden(c.name); })}
|
||||
disabled={allHidden}
|
||||
className="text-xs px-2 py-0.5 rounded bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--border)] transition-colors disabled:opacity-40"
|
||||
>
|
||||
{t("reports.filters.none")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-64 overflow-y-auto px-2 pb-2 space-y-0.5">
|
||||
{filtered.map((cat) => {
|
||||
const visible = !hiddenCategories.has(cat.name);
|
||||
return (
|
||||
<label
|
||||
key={cat.name}
|
||||
className="flex items-center gap-2 px-1.5 py-1 rounded hover:bg-[var(--muted)] cursor-pointer transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visible}
|
||||
onChange={() => onToggleHidden(cat.name)}
|
||||
className="rounded border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)] h-3.5 w-3.5"
|
||||
/>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: cat.color }}
|
||||
/>
|
||||
<span className={`text-xs truncate ${visible ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)] line-through"}`}>
|
||||
{cat.name}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue