feat(rapports) : repli/depliage a chaque niveau de la hierarchie (socle + 3 rapports) #292

Closed
maximus wants to merge 0 commits from issue-288-collapse-socle into main
Owner

Resolves #288

Generated autonomously by /autopilot run of 2026-07-15

Summary

  • Generalizes the report category collapse from level-1-only to every hierarchy level on the 3 hierarchical report tables (real-vs-real Compare, real-vs-budget Compare, Trends by category), via an ancestor-walk visibility algorithm that is independent of row order (fixes the level-ordered budget grid case).
  • Persistence moves from localStorage to user_preferences (per-profile, destroyed with the profile — privacy-first); the hook gains storageKey: null + options.defaultExpanded + async hydration, and a pure tested isCollapsedFor polarity helper.
  • Both render gates fixed (isTopParent -> isParent) so intermediate parents get a truthful chevron + aria-expanded; aria-level added on parent rows; dead CategoryTable.tsx removed. Collapse stays purely visual (subtotals/results from raw rows). 823 vitest green, build + cargo clean.
Resolves #288 Generated autonomously by /autopilot run of 2026-07-15 ## Summary - Generalizes the report category collapse from level-1-only to **every hierarchy level** on the 3 hierarchical report tables (real-vs-real Compare, real-vs-budget Compare, Trends by category), via an **ancestor-walk** visibility algorithm that is independent of row order (fixes the level-ordered budget grid case). - Persistence moves from `localStorage` to `user_preferences` (per-profile, destroyed with the profile — privacy-first); the hook gains `storageKey: null` + `options.defaultExpanded` + async hydration, and a pure tested `isCollapsedFor` polarity helper. - Both render gates fixed (`isTopParent` -> `isParent`) so intermediate parents get a truthful chevron + `aria-expanded`; `aria-level` added on parent rows; dead `CategoryTable.tsx` removed. Collapse stays purely visual (subtotals/results from raw rows). 823 vitest green, build + cargo clean.
maximus added 2 commits 2026-07-16 00:58:37 +00:00
Lessons from the v0.13.0 release (session fdda84cb):
- Step 0: revalidate the tip locally before tagging — check.yml never
  runs on main, and ensure .claude/worktrees/ is empty (vitest recurses)
- Step 9: verify the published release — 7 expected artifacts and
  latest.json content (drives auto-update); status=success is not enough
- Rule: tagging publishes externally via the updater JSON — confirm
  with Max before tagging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(reports): collapse category hierarchy at every level (socle + 3 reports)
All checks were successful
PR Check / rust (pull_request) Successful in 23m0s
PR Check / frontend (pull_request) Successful in 2m30s
48adb3db77
Generalize the report category collapse from level-1-only to every hierarchy
level, on the three hierarchical report tables (real-vs-real Compare,
real-vs-budget Compare, Trends by category). Visibility is now decided by an
ancestor walk, not by row adjacency, so it is independent of row order (the
level-ordered budget grid emits a non-DFS order).

- collapsibleRows: rewrite visibleRows as an ancestor walk (a row is hidden iff
  any ancestor is collapsed); add parentKeyOf + injective `p:` keys;
  collapsibleKeys returns all parents (any depth); extract the pure, tested
  isCollapsedFor polarity helper; MAX_TREE_DEPTH cycle guard.
- useCollapsibleGroups: persist in user_preferences (per-profile, destroyed with
  the profile) instead of localStorage; storageKey nullable (no persistence);
  options.defaultExpanded; async hydration (no flash); collapseAll(rows).
- 3 tables: fix BOTH gates (collapsed flag + button) isTopParent -> isParent, add
  parentKeyOf accessors, aria-level on parent rows.
- Delete dead CategoryTable.tsx (0 imports).
- Tests: rewrite collapsibleRows.test.ts (BFS==DFS masking, cycle guard,
  cross-section ancestor, "(direct)" leaf, polarity); extend overTimeTableModel
  fixture to 3 levels with cascade assertions.

Collapse stays purely visual: subtotals and result lines are computed from raw
rows, never from visible rows.

Resolves #288

Generated autonomously by /autopilot run of 2026-07-15

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-07-16 00:58:50 +00:00
Author
Owner

Revue /pr-review — APPROVE

Verdict : APPROVE (4 suggestions non bloquantes)

La generalisation du repli a chaque niveau est correcte et remarquablement testee. Le coeur du changement — passer d'une detection par adjacence (dependante de l'ordre DFS) a une remontee d'ancetres hiddenByAncestor independante de l'ordre — est le vrai fix du cas grille budget (emise en ordre BFS). Verifie en profondeur :

  • Keying injectif p:<id> + feuille « (direct) » : reportService.ts:642-645 parente la feuille directe sur cat.id avec is_parent:false. Elle partage donc la cle p:<catId> du sous-total, mais seul le sous-total (isParent) est indexe dans la map parents ; la feuille porte la cle du parent via parentKeyOf et se cache/revele avec lui. Exactement le modele des tests.
  • Repli purement visuel : sous-totaux et lignes de resultat calcules sur les lignes brutes (sumLeaves(section.rows), computeResults(data)), jamais sur groups.visible(). Aucune atteinte a l'exactitude financiere. Les tests l'ancrent (« collapse is purely visual »).
  • Persistance reellement par-profil : switchProfile dispatch INCREMENT_REFRESHApp.tsx:108 <BrowserRouter key={refreshKey}> remonte tout le sous-arbre → l'effet d'hydratation rejoue getPreference sur la DB du nouveau profil. Aucune fuite d'etat entre profils, ecritures toujours sur le profil actif. La promesse « detruit avec le profil » tient.
  • Cles de persistance reports-{compare,bva,trends}-expanded bien namespacees — aucune collision avec les cles user_preferences existantes.
  • Suppression de CategoryTable.tsx : reellement mort, aucun import ailleurs.
  • Robustesse : garde de cycle MAX_TREE_DEPTH, ancetre absent → visible (feuille orpheline preservee), polarite isCollapsedFor dans les deux sens — tous couverts. Le test « BFS masque a l'identique de DFS » est le regression test du bug corrige.
  • CHANGELOG FR + EN a jour ; setPreference en SQL parametre ($1/$2) ; pas de secret, pas de nouvelle migration.

Suggestions (non bloquantes)

  1. Course d'hydratation (useCollapsibleGroups.ts) — si l'utilisateur toggle un groupe dans la fenetre (~ms) entre le mount et la resolution de getPreference, le .then d'hydratation ecrase son toggle (setFlipped(parseStoredExpanded(raw))). Fenetre minuscule et auto-corrigee au clic suivant, mais un garde « l'utilisateur a deja interagi » (ou n'hydrater que si flipped est encore intact) fermerait le trou proprement.

  2. Generalite speculative (YAGNI)defaultExpanded: true et storageKey: null sont ajoutes + testes, mais aucun consommateur en production dans cette PR : les 3 tables passent le defaut (defaultExpanded: false) et une vraie cle. Infrastructure construite « pour les arbres de categories » qui n'existent pas encore. Non nuisible, mais a defaut d'un consommateur, elle pourrait attendre.

  3. Scope creep — les modifs de .claude/skills/release/SKILL.md (etapes pre-vol/post-CI, « confirmer avec Max avant de tagger ») sont sans lien avec #288 ; elles proviennent d'une autre session (release v0.13.0). Idealement un commit/PR separe.

  4. aria-level inerte (a11y mineur) — sur un <tr> d'une <table> ordinaire, sans role="treegrid"/row/treeitem, aria-level est ignore par les lecteurs d'ecran, et seuls les parents le portent. aria-expanded sur le bouton, lui, est effectif — les chevrons sont bien annonces, donc l'essentiel de l'a11y est la.

Aucun blocage : correction correcte, purement visuelle sur les chiffres, tests solides (incl. regression BFS/DFS). Bon a merger.

## Revue /pr-review — APPROVE **Verdict : APPROVE** (4 suggestions non bloquantes) La generalisation du repli a chaque niveau est correcte et remarquablement testee. Le coeur du changement — passer d'une detection par adjacence (dependante de l'ordre DFS) a une remontee d'ancetres `hiddenByAncestor` independante de l'ordre — est le vrai fix du cas grille budget (emise en ordre BFS). Verifie en profondeur : - **Keying injectif `p:<id>` + feuille « (direct) »** : `reportService.ts:642-645` parente la feuille directe sur `cat.id` avec `is_parent:false`. Elle partage donc la cle `p:<catId>` du sous-total, mais seul le sous-total (isParent) est indexe dans la map `parents` ; la feuille porte la cle du parent via `parentKeyOf` et se cache/revele avec lui. Exactement le modele des tests. - **Repli purement visuel** : sous-totaux et lignes de resultat calcules sur les lignes brutes (`sumLeaves(section.rows)`, `computeResults(data)`), jamais sur `groups.visible()`. Aucune atteinte a l'exactitude financiere. Les tests l'ancrent (« collapse is purely visual »). - **Persistance reellement par-profil** : `switchProfile` dispatch `INCREMENT_REFRESH` → `App.tsx:108` `<BrowserRouter key={refreshKey}>` remonte tout le sous-arbre → l'effet d'hydratation rejoue `getPreference` sur la DB du nouveau profil. Aucune fuite d'etat entre profils, ecritures toujours sur le profil actif. La promesse « detruit avec le profil » tient. - **Cles de persistance** `reports-{compare,bva,trends}-expanded` bien namespacees — aucune collision avec les cles `user_preferences` existantes. - **Suppression de `CategoryTable.tsx`** : reellement mort, aucun import ailleurs. - **Robustesse** : garde de cycle `MAX_TREE_DEPTH`, ancetre absent → visible (feuille orpheline preservee), polarite `isCollapsedFor` dans les deux sens — tous couverts. Le test « BFS masque a l'identique de DFS » est le regression test du bug corrige. - CHANGELOG FR + EN a jour ; `setPreference` en SQL parametre ($1/$2) ; pas de secret, pas de nouvelle migration. ### Suggestions (non bloquantes) 1. **Course d'hydratation** (`useCollapsibleGroups.ts`) — si l'utilisateur toggle un groupe dans la fenetre (~ms) entre le mount et la resolution de `getPreference`, le `.then` d'hydratation ecrase son toggle (`setFlipped(parseStoredExpanded(raw))`). Fenetre minuscule et auto-corrigee au clic suivant, mais un garde « l'utilisateur a deja interagi » (ou n'hydrater que si `flipped` est encore intact) fermerait le trou proprement. 2. **Generalite speculative (YAGNI)** — `defaultExpanded: true` et `storageKey: null` sont ajoutes + testes, mais aucun consommateur en production dans cette PR : les 3 tables passent le defaut (`defaultExpanded: false`) et une vraie cle. Infrastructure construite « pour les arbres de categories » qui n'existent pas encore. Non nuisible, mais a defaut d'un consommateur, elle pourrait attendre. 3. **Scope creep** — les modifs de `.claude/skills/release/SKILL.md` (etapes pre-vol/post-CI, « confirmer avec Max avant de tagger ») sont sans lien avec #288 ; elles proviennent d'une autre session (release v0.13.0). Idealement un commit/PR separe. 4. **`aria-level` inerte** (a11y mineur) — sur un `<tr>` d'une `<table>` ordinaire, sans `role="treegrid"`/`row`/`treeitem`, `aria-level` est ignore par les lecteurs d'ecran, et seuls les parents le portent. `aria-expanded` sur le bouton, lui, est effectif — les chevrons sont bien annonces, donc l'essentiel de l'a11y est la. _Aucun blocage : correction correcte, purement visuelle sur les chiffres, tests solides (incl. regression BFS/DFS). Bon a merger._
Author
Owner

Mergé en fast-forward dans main (pile #292→#295), commit 48adb3d. Forgejo ne détecte pas le merge local → fermeture manuelle. L'issue liée s'est auto-fermée via Resolves #N.

Mergé en fast-forward dans `main` (pile #292→#295), commit `48adb3d`. Forgejo ne détecte pas le merge local → fermeture manuelle. L'issue liée s'est auto-fermée via `Resolves #N`.
maximus closed this pull request 2026-07-18 18:47:38 +00:00
All checks were successful
PR Check / rust (pull_request) Successful in 23m0s
PR Check / frontend (pull_request) Successful in 2m30s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#292
No description provided.