docs(collapse): ADR 0016 per-profile UI state + guide, architecture, i18n

Document the final multi-level category collapse behavior (shipped in
#288/#289/#290) and the structural decision to persist per-profile UI
state in user_preferences rather than localStorage.

- ADR 0016 (accepted): profile-specific UI state (category collapse) lives
  in the profile's own SQLite user_preferences table, not localStorage.
  deleteProfile drops the .db but purges no localStorage, so a per-profile
  localStorage key would be a residue surviving profile deletion, leaking
  which categories a (possibly PIN-protected) profile explored. States the
  boundary: profile-specific -> user_preferences; machine-global (theme,
  subtotals position, Cartes period mode) -> localStorage.
- guide-utilisateur.md sections 8 (Budget) + 9 (Reports): multi-level
  collapse, collapsed-by-default, "Expand all / Collapse all" button,
  memory per profile.
- docs.* i18n keys (fr + en): mirror the guide additions in the in-app help
  page (docs.budget + docs.reports features/tips).
- architecture.md: user_preferences line now names the 4 collapse keys,
  useCollapsibleGroups cross-cutting hook note, ADR index row.

No DB migration, no behavior change (docs only). Build clean, 828 vitest.

Resolves #291

Generated autonomously by /autopilot run of 2026-07-15
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-07-15 21:43:44 -04:00
parent 9f628aa9f4
commit e3ecfe7a6c
5 changed files with 117 additions and 8 deletions

View file

@ -0,0 +1,87 @@
# ADR 0016 — Persistance de l'état UI par profil : le repli des catégories vit dans `user_preferences`
- Status: **Accepted**
- Date: 2026-07-15
- Issues: #288 (socle multi-niveaux + 3 rapports + bascule vers `user_preferences`), #289 (grille budget), #290 (unification des deux arbres de catégories), #291 (cet ADR + doc)
- Spec: [`spec-decisions-collapse-multi-niveaux.md`](../../spec-decisions-collapse-multi-niveaux.md), [`spec-plan-collapse-multi-niveaux.md`](../../spec-plan-collapse-multi-niveaux.md) (plan v2)
- S'appuie sur [ADR 0005](0005-multi-profile-db.md) (bases SQLite séparées par profil) et [ADR 0002](0002-useReducer-vs-redux.md) (état local, pas de store global)
## Contexte
Le repli/dépli des catégories a d'abord été introduit sur les deux rapports comparables (issue #254/#265) : un `Set` d'ID de catégories par rapport, persisté en **`localStorage`**, replié par défaut. La généralisation (issues #288/#289/#290) a étendu ce mécanisme à **tous les niveaux** de la hiérarchie et à **six surfaces** : les trois tableaux de rapports hiérarchiques (comparable réel-vs-réel `ComparePeriodTable`, réel-vs-budget `BudgetVsActualTable`, tendance par catégorie `CategoryOverTimeTable`), la **grille budgétaire** (`BudgetTable`), et les **deux arbres de gestion des catégories** (`CategoryTree` de la page Catégories + `CategoryTaxonomyTree` du guide standard et de l'assistant de migration).
Cette généralisation a forcé une question qui n'était pas tranchée en #254 : **où vit l'état de repli ?** Deux propriétés du produit rendent le choix structurant, pas cosmétique.
1. **L'app est multi-profils à bases séparées** ([ADR 0005](0005-multi-profile-db.md)) : chaque profil a sa **propre base SQLite**, ses propres catégories, ses propres ID de catégories. Un profil peut être **protégé par un NIP** (Argon2). Supprimer un profil = **supprimer son fichier `.db`** (`deleteProfileDb` → commande Rust `delete_profile_db`) + retirer son entrée de `profiles.json`. Rien d'autre n'est nettoyé — `deleteProfile` (`ProfileContext`) ne touche **jamais** `localStorage`.
2. **`localStorage` est machine-global**, partagé par tous les profils du poste. La clé de repli est un ensemble d'**ID de catégories** (`p:<categoryId>`), et ces ID n'ont de sens que **dans la base d'un profil donné** : le même ID numérique désigne des catégories différentes selon le profil. Une clé de repli en `localStorage` serait donc soit **en collision entre profils** (mêmes ID, catégories différentes), soit suffixée par `profileId` — et dans ce dernier cas, elle **survivrait à la suppression du profil**, devenant un **résidu**.
Ce résidu n'est pas anodin : l'ensemble des catégories qu'un profil a **dépliées** révèle quelles postes de dépense l'utilisateur a explorés. Le laisser traîner en `localStorage` après la suppression d'un profil (potentiellement NIP-protégé) trahit une part de son comportement financier. C'est **incompatible avec le principe privacy-first** (non négociable) du projet.
## Décision
**L'état UI *spécifique à un profil* est persisté dans la table `user_preferences` de la base du profil ; l'état UI *machine-global* reste dans `localStorage`.** Le repli des catégories relève du premier cas.
### 1. Frontière `user_preferences` vs `localStorage`
| Nature de l'état | Support | Pourquoi | Exemples |
|---|---|---|---|
| **Spécifique au profil** (dépend des données du profil, ou révèle son usage) | `user_preferences` (base SQLite du profil) | Scoping par-profil **gratuit** (la clé vit dans la base du profil) ; **détruit avec le profil** (drop du `.db`) ⇒ zéro résidu | **Repli des catégories** des rapports + grille budget (clés `reports-compare-expanded`, `reports-bva-expanded`, `reports-trends-expanded`, `budget-grid-expanded`) ; `import_folder`, `balance_show_returns`, `balance_starter_proposed` (préexistants) |
| **Machine-globale** (scalaire agnostique du profil, ne révèle rien de sensible) | `localStorage` | On *veut* la même valeur quel que soit le profil ouvert ; aucune fuite si elle survit à une suppression | Thème clair/sombre (`theme`), position des sous-totaux (`*-subtotals-position`), mode de période Cartes (`reports-cartes-period-mode`), bascule graphique/tableau par rapport |
Le critère : une préférence est **machine-globale** quand sa valeur est un scalaire agnostique du profil (`"top"`/`"bottom"`, `"dark"`, `"chart"`) qu'on souhaite partager entre tous les profils du poste. Elle est **spécifique au profil** dès qu'elle est indexée par des identifiants propres à la base du profil (les ID de catégories du repli) **ou** qu'elle expose l'usage d'un profil donné.
### 2. Mécanisme — `useCollapsibleGroups(storageKey: string | null)`
Le hook `useCollapsibleGroups` porte cette décision via un unique paramètre `storageKey` :
- **`storageKey` non nul** ⇒ persistance dans `user_preferences` via `userPreferenceService` (`getPreference`/`setPreference`, UPSERT sur `key`). Utilisé par les **quatre surfaces persistées** (3 rapports + budget). La valeur sérialisée est le `Set` des clés dont l'état **diffère du défaut** — ce qui permet un défaut « tout replié » sans graine (Set vide = tout replié pour `defaultExpanded: false`).
- **`storageKey === null`** ⇒ **aucune persistance**, état purement en mémoire, réinitialisé au défaut à chaque montage. Utilisé par les **deux arbres de catégories** (page Catégories, guide standard, assistant de migration) : leur repli est un confort de navigation éphémère, sans intérêt à conserver ni à révéler.
Le hook reste **sans contexte** — il n'appelle jamais `useProfile()` : la clé `user_preferences` est **déjà** par-profil, puisqu'elle vit dans la base du profil actif. Aucun `profileId` à faufiler.
### 3. Rationale privacy — le résidu qui survit à la suppression
C'est l'argument central. `deleteProfile` détruit la base (`.db`) mais **ne purge aucun `localStorage`**. Une clé de repli par-profil en `localStorage` (ex. `reports-compare-expanded:<profileId>`) survivrait donc à la suppression du profil, laissant deviner **quelles catégories** un profil désormais supprimé — et peut-être NIP-protégé — avait explorées. En logeant l'état dans `user_preferences`, il **disparaît avec le fichier `.db`** : la suppression du profil emporte automatiquement son état UI, sans code de nettoyage dédié et sans surface de fuite. La propriété « supprimer un profil = supprimer un fichier » ([ADR 0005](0005-multi-profile-db.md)) est préservée telle quelle.
## Alternatives considérées
### A. `localStorage` suffixé par `profileId` — **rejeté**
Garder le repli en `localStorage` avec une clé `…:<profileId>` par profil. Rejeté : c'est précisément le **résidu** décrit ci-dessus (survit à `deleteProfile`, fuite privacy), et il faudrait ajouter un balayage de nettoyage dans `deleteProfile` — du code fragile qui doit connaître toutes les clés UI par-profil de l'app, à maintenir à chaque nouvelle surface repliable.
### B. Table SQLite dédiée (`ui_collapse_state`) — rejeté
Créer une table par-profil rien que pour le repli. Rejeté : sur-dimensionné. `user_preferences` est déjà une table clé-valeur par-profil faite pour ça (`import_folder`, `balance_show_returns`…) ; une clé par surface suffit, sans migration.
### C. Tout en mémoire (aucune persistance) — rejeté pour les rapports/budget
Simple, zéro résidu. Rejeté pour les **quatre** surfaces persistées : sur des tableaux profonds, re-replier à chaque visite est une friction réelle (finding remonté en #254). **Retenu en revanche pour les deux arbres de catégories** (`storageKey: null`) — d'où le paramètre nullable plutôt qu'un choix uniforme.
## Consequences
### Positives
- **Zéro résidu, zéro fuite** : l'état de repli est détruit avec le `.db` du profil ; aucune trace machine-globale de l'usage d'un profil supprimé. Conforme privacy-first.
- **Scoping par-profil gratuit** : la clé vit dans la base du profil ⇒ isolation automatique, hook sans contexte, pas de `profileId` à propager.
- **Réutilise l'existant** : `user_preferences` + `userPreferenceService` déjà en place ⇒ **aucune migration, aucune table, aucun changement de schéma**.
- **Frontière tracée** : la règle « profil-spécifique → `user_preferences` ; machine-globale → `localStorage` » est explicite (§1) ⇒ le prochain état UI par-profil ne repartira pas en `localStorage` par défaut (caveat de review levé).
### Négatives / risques actés
- **Hydratation asynchrone** : `getPreference`/`setPreference` sont `async` (passent par `getDb()`). L'état initial rendu est le **défaut** (tout replié pour les rapports/budget), puis un `useEffect` hydrate depuis `user_preferences`. **Pas de flash visible** : le défaut EST ce qui s'affiche en premier, et l'hydratation ne peut que **révéler** ce que l'utilisateur avait déplié (jamais masquer ce qui était visible). Acceptable — c'est la contrepartie assumée du choix.
- **Best-effort** : une lecture/écriture en échec retombe silencieusement sur l'état par défaut (jamais de crash). Une écriture perdue = la session suivante rouvre au défaut.
- **Frontière à faire respecter dans la durée** : rien dans le type ne force un futur état par-profil à choisir `user_preferences`. La règle est documentaire (cet ADR + `docs/architecture.md`) — d'où l'importance de l'avoir tracée.
### Neutre
- Les préférences machine-globales existantes (`theme`, `*-subtotals-position`, `reports-cartes-period-mode`, bascule graphique/tableau) **restent en `localStorage`** : elles sont volontairement partagées entre profils et ne révèlent rien de sensible en cas de suppression. Aucune migration de ces clés n'est entreprise.
## Liens
- [ADR 0005](0005-multi-profile-db.md) — bases SQLite séparées par profil ; « supprimer un profil = supprimer un fichier `.db` » (fondement de l'argument résidu)
- [ADR 0002](0002-useReducer-vs-redux.md) — état local par domaine, pas de store global (le repli reste un état UI local persisté à la marge)
- `src/hooks/useCollapsibleGroups.ts` — implémentation (`storageKey: string | null`, `defaultExpanded`, hydratation async)
- `src/utils/collapsibleRows.ts` — helpers purs (marche des ancêtres, polarité du `Set`)
- `src/services/userPreferenceService.ts``getPreference`/`setPreference` (UPSERT sur `user_preferences`)
- Issues #254/#265 (repli d'origine, localStorage) → #288/#289/#290 (généralisation + bascule `user_preferences`) → #291 (cette doc)

View file

@ -89,7 +89,7 @@ simpl-resultat/
| `budget_templates` | Modèles de budget réutilisables | | `budget_templates` | Modèles de budget réutilisables |
| `budget_template_entries` | Catégories et montants dans les modèles | | `budget_template_entries` | Catégories et montants dans les modèles |
| `import_config_templates` | Modèles prédéfinis de config d'import | | `import_config_templates` | Modèles prédéfinis de config d'import |
| `user_preferences` | Préférences applicatives (clé-valeur) | | `user_preferences` | Préférences applicatives (clé-valeur) `import_folder`, `balance_show_returns`, `balance_starter_proposed`**et l'état de repli des catégories** (clés `reports-compare-expanded`, `reports-bva-expanded`, `reports-trends-expanded`, `budget-grid-expanded`) : persisté ici **par profil** plutôt qu'en `localStorage`, pour être détruit avec le profil (pas de résidu machine-global survivant à la suppression) — voir [ADR 0016](adr/0016-persistance-etat-ui-par-profil.md) |
| `balance_categories` | Taxonomie des **classes d'actif** (Liquidités, Fonds/FNB, Actions, Crypto, Autres) — `kind ∈ {simple, priced}` (défaut suggéré pour les nouveaux comptes), `custom_label` pour le renommage bilingue-safe (v12). Les ex-types véhicules (TFSA/RRSP) ont migré vers `balance_accounts.vehicle_type` (Étape 1, v12/v13, [ADR 0014](adr/0014-balance-vehicule-attribut.md)) | | `balance_categories` | Taxonomie des **classes d'actif** (Liquidités, Fonds/FNB, Actions, Crypto, Autres) — `kind ∈ {simple, priced}` (défaut suggéré pour les nouveaux comptes), `custom_label` pour le renommage bilingue-safe (v12). Les ex-types véhicules (TFSA/RRSP) ont migré vers `balance_accounts.vehicle_type` (Étape 1, v12/v13, [ADR 0014](adr/0014-balance-vehicule-attribut.md)) |
| `balance_accounts` | Comptes de bilan (rattachés à une catégorie). `currency` hardcodée à `CAD` au MVP via CHECK. `archived_at` pour soft-delete. `vehicle_type` (enveloppe fiscale nullable, v12, [ADR 0014](adr/0014-balance-vehicule-attribut.md)). `kind ∈ {simple, detailed}` + `detailed_since` (pivot faisant autorité, v15, [ADR 0015](adr/0015-balance-detail-par-titre.md)) — porte désormais l'axe simple/détaillé (auparavant dérivé de `category.kind`). **Issue #179** : 4 comptes de départ seedés (`consolidated_schema.sql`) + proposés aux profils existants via `StarterAccountsModal` | | `balance_accounts` | Comptes de bilan (rattachés à une catégorie). `currency` hardcodée à `CAD` au MVP via CHECK. `archived_at` pour soft-delete. `vehicle_type` (enveloppe fiscale nullable, v12, [ADR 0014](adr/0014-balance-vehicule-attribut.md)). `kind ∈ {simple, detailed}` + `detailed_since` (pivot faisant autorité, v15, [ADR 0015](adr/0015-balance-detail-par-titre.md)) — porte désormais l'axe simple/détaillé (auparavant dérivé de `category.kind`). **Issue #179** : 4 comptes de départ seedés (`consolidated_schema.sql`) + proposés aux profils existants via `StarterAccountsModal` |
| `balance_snapshots` | Snapshots datés (`snapshot_date` UNIQUE) — éditer = mettre à jour les lignes, pas dupliquer | | `balance_snapshots` | Snapshots datés (`snapshot_date` UNIQUE) — éditer = mettre à jour les lignes, pas dupliquer |
@ -221,6 +221,13 @@ Chaque hook encapsule la logique d'état via `useReducer` :
| `useLicense` | État de la licence et entitlements | | `useLicense` | État de la licence et entitlements |
| `useAuth` | Authentification Compte Maximus (OAuth2 PKCE, subscription status) | | `useAuth` | Authentification Compte Maximus (OAuth2 PKCE, subscription status) |
### Hook transverse — `useCollapsibleGroups`
`useCollapsibleGroups<T>(storageKey: string | null, accessors, { defaultExpanded })` est un hook **utilitaire d'état UI** (basé `useState`, hors convention `useReducer` par domaine) partagé par toutes les surfaces à hiérarchie repliable : les 3 tableaux de rapports (`ComparePeriodTable`, `BudgetVsActualTable`, `CategoryOverTimeTable`), la grille budget (`BudgetTable`) et les 2 arbres de catégories (`CategoryTree`, `CategoryTaxonomyTree`). Les helpers purs (marche des ancêtres pour la visibilité, polarité du `Set` selon `defaultExpanded`) vivent dans `src/utils/collapsibleRows.ts`.
- `storageKey` **non nul** → l'état de repli est **persisté par profil** dans `user_preferences` (via `userPreferenceService`), donc détruit avec le profil, sans résidu `localStorage` ([ADR 0016](adr/0016-persistance-etat-ui-par-profil.md)). Quatre surfaces persistées (les 3 rapports + budget). Hydratation **asynchrone** : le défaut (« tout replié » pour rapports/budget) est rendu d'abord, un `useEffect` hydrate ensuite → pas de flash visible.
- `storageKey === null` → état **purement en mémoire**, réinitialisé à chaque montage (les 2 arbres de catégories : navigation éphémère, rien à conserver ni à révéler).
## Commandes Tauri (36) ## Commandes Tauri (36)
### `fs_commands.rs` — Système de fichiers (6) ### `fs_commands.rs` — Système de fichiers (6)
@ -429,3 +436,4 @@ Les ADRs documentent les décisions techniques structurantes. Ils vivent dans `d
| [0013](adr/0013-stocks-provider-evaluation.md) | Évaluation provider stocks : Alpha Vantage retenu comme cible | 2026-05-09 | Accepted | | [0013](adr/0013-stocks-provider-evaluation.md) | Évaluation provider stocks : Alpha Vantage retenu comme cible | 2026-05-09 | Accepted |
| [0014](adr/0014-balance-vehicule-attribut.md) | Bilan : le véhicule fiscal est un attribut du compte (Étape 1) | 2026-06-01 | Accepted | | [0014](adr/0014-balance-vehicule-attribut.md) | Bilan : le véhicule fiscal est un attribut du compte (Étape 1) | 2026-06-01 | Accepted |
| [0015](adr/0015-balance-detail-par-titre.md) | Bilan : détail par titre (holdings par snapshot, Étape 2) | 2026-06-06 | Accepted | | [0015](adr/0015-balance-detail-par-titre.md) | Bilan : détail par titre (holdings par snapshot, Étape 2) | 2026-06-06 | Accepted |
| [0016](adr/0016-persistance-etat-ui-par-profil.md) | Persistance de l'état UI par profil : repli des catégories dans `user_preferences` | 2026-07-15 | Accepted |

View file

@ -226,6 +226,7 @@ Planifiez votre budget mensuel pour chaque catégorie et suivez le prévu par ra
- Répartition égale du montant annuel sur 12 mois - Répartition égale du montant annuel sur 12 mois
- Modèles de budget pour sauvegarder et appliquer des configurations - Modèles de budget pour sauvegarder et appliquer des configurations
- Sous-totaux par catégorie parente - Sous-totaux par catégorie parente
- Repli / dépli de la hiérarchie à **chaque niveau** : chaque catégorie parente a son chevron, la grille s'ouvre **entièrement repliée** (seules les catégories de premier niveau visibles), et un bouton **« Tout déplier / Tout replier »** ouvre ou ferme tous les niveaux d'un coup — votre choix est mémorisé **par profil**
- En-têtes de colonnes fixes au défilement vertical - En-têtes de colonnes fixes au défilement vertical
### Comment faire ### Comment faire
@ -241,6 +242,7 @@ Planifiez votre budget mensuel pour chaque catégorie et suivez le prévu par ra
- La colonne Annuel additionne automatiquement les 12 mois — un avertissement apparaît si les totaux mensuels ne correspondent pas - La colonne Annuel additionne automatiquement les 12 mois — un avertissement apparaît si les totaux mensuels ne correspondent pas
- Les modèles peuvent être appliqués à des mois spécifiques ou aux 12 mois d'un coup - Les modèles peuvent être appliqués à des mois spécifiques ou aux 12 mois d'un coup
- Les catégories parentes affichent les sous-totaux agrégés de leurs enfants - Les catégories parentes affichent les sous-totaux agrégés de leurs enfants
- Replier une catégorie est **purement visuel** : les sous-totaux et les totaux annuels sont toujours calculés sur toutes les lignes, jamais seulement sur les lignes visibles — aucun montant saisi n'est perdu quand vous repliez
--- ---
@ -319,7 +321,7 @@ Toujours mensuelle, indépendamment du toggle.
### Rapport Tendances (`/reports/trends`) ### Rapport Tendances (`/reports/trends`)
- **Flux global** : revenus vs dépenses vs solde net sur la période, en graphique d'aires ou tableau - **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 - **Par catégorie** : évolution de chaque catégorie, en lignes ou tableau. En mode tableau, la hiérarchie se **replie / déplie à chaque niveau** (voir ci-dessous)
### Rapport Comparables (`/reports/compare`) ### Rapport Comparables (`/reports/compare`)
@ -329,6 +331,10 @@ Trois modes accessibles via un tab bar :
- **Année vs année précédente** — même logique sur 12 mois vs 12 mois - **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 - **Réel vs budget** — reprend la vue Budget vs Réel avec ses totaux mensuels et cumul annuel
#### Repli de la hiérarchie
Les trois modes ci-dessus, comme le tableau **Par catégorie** des Tendances, présentent les catégories en arbre repliable **à chaque niveau**. Chaque catégorie parente — y compris les niveaux intermédiaires — porte son propre chevron : le déplier révèle ses enfants directs (eux-mêmes repliés), de sorte que vous descendez d'un niveau à la fois. Le tableau s'ouvre **entièrement replié** (seules les catégories de premier niveau visibles) ; le bouton **« Tout déplier / Tout replier »** ouvre ou ferme tous les niveaux d'un coup. Replier une catégorie est **purement visuel** — les sous-totaux, totaux et lignes de résultat restent calculés sur l'ensemble des lignes, jamais sur les seules lignes visibles. Vos choix de repli sont **mémorisés par profil** (dans la base du profil, ils disparaissent donc avec lui).
### Rapport Zoom catégorie (`/reports/category`) ### 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 : 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 :

View file

@ -914,6 +914,7 @@
"Split annual amount evenly across 12 months", "Split annual amount evenly across 12 months",
"Budget templates to save and apply configurations", "Budget templates to save and apply configurations",
"Parent category subtotals", "Parent category subtotals",
"Collapse/expand the hierarchy at every level: each parent category has its own chevron, the grid opens fully collapsed, and an \"Expand all / Collapse all\" button opens or closes every level at once — remembered per profile",
"Column headers stay fixed when scrolling vertically" "Column headers stay fixed when scrolling vertically"
], ],
"steps": [ "steps": [
@ -926,7 +927,8 @@
"tips": [ "tips": [
"The Annual column auto-sums all 12 months — a warning appears if monthly totals don't match", "The Annual column auto-sums all 12 months — a warning appears if monthly totals don't match",
"Templates can be applied to specific months or all 12 at once", "Templates can be applied to specific months or all 12 at once",
"Parent categories show subtotals aggregated from their children" "Parent categories show subtotals aggregated from their children",
"Collapsing a category is purely visual: subtotals and annual totals are always computed over all rows, never just the visible ones — no amount you typed is ever lost"
] ]
}, },
"reports": { "reports": {
@ -941,7 +943,8 @@
"Category Zoom: single-category drill-down with donut, monthly evolution, and filterable transaction table; auto-rollup of subcategories", "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", "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", "SVG patterns (lines, dots, crosshatch) to distinguish categories",
"View mode preference (chart vs. table) persisted per report section" "View mode preference (chart vs. table) persisted per report section",
"Collapse the category hierarchy at every level on the Compare reports and the by-category Trends table: a chevron per parent category, opens fully collapsed, an \"Expand all / Collapse all\" button; purely visual (subtotals and results unchanged), remembered per profile"
], ],
"steps": [ "steps": [
"Open /reports to see the highlights panel and four navigation cards", "Open /reports to see the highlights panel and four navigation cards",
@ -957,7 +960,8 @@
"Keywords must be 264 characters long", "Keywords must be 264 characters long",
"The Category Zoom is protected against malformed category trees: a parent_id cycle cannot freeze the app", "The Category Zoom is protected against malformed category trees: a parent_id cycle cannot freeze the app",
"On /reports/cartes in YTD mode, the MoM delta for January is always \"—\" (no prior YTD window inside the same year), and the savings rate stays \"—\" when YTD income is zero", "On /reports/cartes in YTD mode, the MoM delta for January is always \"—\" (no prior YTD window inside the same year), and the savings rate stays \"—\" when YTD income is zero",
"Seasonality, top movers, and budget adherence stay monthly even when the toggle is set to YTD — only the 4 KPI numbers change" "Seasonality, top movers, and budget adherence stay monthly even when the toggle is set to YTD — only the 4 KPI numbers change",
"Your category collapse choices (Compare, Trends, Budget) are remembered per profile: stored in the profile's own database, they are destroyed with it"
] ]
}, },
"balance": { "balance": {

View file

@ -914,6 +914,7 @@
"Répartition égale du montant annuel sur 12 mois", "Répartition égale du montant annuel sur 12 mois",
"Modèles de budget pour sauvegarder et appliquer des configurations", "Modèles de budget pour sauvegarder et appliquer des configurations",
"Sous-totaux par catégorie parente", "Sous-totaux par catégorie parente",
"Repli / dépli de la hiérarchie à chaque niveau : chaque catégorie parente a son chevron, la grille s'ouvre entièrement repliée, et un bouton « Tout déplier / Tout replier » ouvre ou ferme tous les niveaux d'un coup — mémorisé par profil",
"En-têtes de colonnes fixes au défilement vertical" "En-têtes de colonnes fixes au défilement vertical"
], ],
"steps": [ "steps": [
@ -926,7 +927,8 @@
"tips": [ "tips": [
"La colonne Annuel additionne automatiquement les 12 mois — un avertissement apparaît si les totaux mensuels ne correspondent pas", "La colonne Annuel additionne automatiquement les 12 mois — un avertissement apparaît si les totaux mensuels ne correspondent pas",
"Les modèles peuvent être appliqués à des mois spécifiques ou aux 12 mois d'un coup", "Les modèles peuvent être appliqués à des mois spécifiques ou aux 12 mois d'un coup",
"Les catégories parentes affichent les sous-totaux agrégés de leurs enfants" "Les catégories parentes affichent les sous-totaux agrégés de leurs enfants",
"Replier une catégorie est purement visuel : les sous-totaux et les totaux annuels sont toujours calculés sur toutes les lignes, jamais sur les seules lignes visibles — aucun montant saisi n'est perdu"
] ]
}, },
"reports": { "reports": {
@ -941,7 +943,8 @@
"Zoom catégorie : analyse d'une seule catégorie avec donut, évolution mensuelle et tableau de transactions filtrable ; rollup automatique des sous-catégories", "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", "É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", "Motifs SVG (lignes, points, hachures) pour distinguer les catégories",
"Préférence chart/table mémorisée par section de rapport" "Préférence chart/table mémorisée par section de rapport",
"Repli de la hiérarchie à chaque niveau sur les Comparables et la tendance Par catégorie : chevron par catégorie parente, ouverture entièrement repliée, bouton « Tout déplier / Tout replier » ; purement visuel (sous-totaux et résultats inchangés), mémorisé par profil"
], ],
"steps": [ "steps": [
"Ouvrez /reports pour voir le panneau de faits saillants et les quatre cartes de navigation", "Ouvrez /reports pour voir le panneau de faits saillants et les quatre cartes de navigation",
@ -957,7 +960,8 @@
"Les mots-clés doivent faire entre 2 et 64 caractères", "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", "Le Zoom catégorie est protégé contre les arborescences malformées : un cycle parent_id ne peut pas figer l'app",
"Sur /reports/cartes en mode YTD, le delta MoM du mois de janvier est toujours « — » (pas de fenêtre YTD antérieure dans la même année), et le taux d'épargne reste « — » quand les revenus YTD sont à zéro", "Sur /reports/cartes en mode YTD, le delta MoM du mois de janvier est toujours « — » (pas de fenêtre YTD antérieure dans la même année), et le taux d'épargne reste « — » quand les revenus YTD sont à zéro",
"La saisonnalité, les top mouvements et l'adhésion budgétaire restent mensuels même quand le toggle est sur YTD — seuls les 4 chiffres KPI changent" "La saisonnalité, les top mouvements et l'adhésion budgétaire restent mensuels même quand le toggle est sur YTD — seuls les 4 chiffres KPI changent",
"Vos choix de repli des catégories (Comparables, Tendances, Budget) sont mémorisés par profil : stockés dans la base du profil, ils disparaissent avec lui"
] ]
}, },
"balance": { "balance": {