docs: the hierarchy window reads the categories again on opening (#393)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-10-19 20:59:27 -04:00
parent 7f25293669
commit a5b92b975e

View file

@ -283,7 +283,7 @@ Un `parent_id` en boucle (A sous B, B sous A, ou une catégorie sous elle-même)
- **Diagnostic** — `findOrphanCategories` lit **toutes** les catégories, actives ou non (`SELECT id, parent_id, is_active FROM categories`) : les autres requêtes ne gardent que les actives, ce qui ferait d'un parent désactivé un parent absent. Motifs : `cycle` (le membre de plus petit `id`, une entrée par boucle), `inactive_parent`, `missing_parent` ; seule la tête de chaque groupe est listée, sa descendance la suit.
- **Validation** — `updateCategory` (seulement quand le parent change : un renommage d'une catégorie enregistrée avant ces règles n'est jamais refusé) et `reattachCategory` (toujours) passent par `checkNewParent` : `parentChangeRefusal` sur les catégories actives (`self_parent` ; `cycle` pour un parent pris dans la descendance ou sans profondeur ; `too_deep`), puis ce que porte le parent (`checkParentContent`, décision de Max du 2026-10-19), dans cet ordre, le premier trouvé faisant le refus : `parent_has_transactions` (des transactions directes), `parent_has_budget` (un montant non nul dans `budget_entries` ou `budget_template_entries`), `parent_has_keywords` (un mot-clé actif ; un mot-clé supprimé, `is_active = 0`, ne classe rien et ne compte pas). Devenu parent, il ne serait plus saisissable : ses transactions resteraient dessus, ses montants sortiraient de la grille du budget, et ses mots-clés continueraient d'y classer des transactions. Le contenu se vérifie quel que soit l'état du parent, feuille ou déjà parent. `createCategory` vérifie de même le parent d'une sous-catégorie, dans un `withTransaction` qui porte aussi l'insertion et le `is_inputable = 0` du parent ; sa vérification de profondeur (`getCategoryDepth`, erreur simple) reste avant la transaction. La racine est toujours acceptée : c'est la réparation de dernier recours d'une boucle. Un refus lève `CategoryHierarchyError` (`code`), que `useCategories` reconnaît par `instanceof` et affiche par la clé `categories.hierarchy.errors.<code>`. Lectures et écritures (la ligne, puis `is_inputable = 0` sur le nouveau parent, comme `createCategory`) se font dans **un** `withTransaction`, par son handle. Un parent inactif ou inexistant lève une erreur simple, hors d'atteinte de l'interface.
- **`getCategoryDepth`** garde les `id` vus et rend `Infinity` sur une boucle : `createCategory` refuse alors ce parent.
- **Fenêtre** — `HierarchyCheckModal` (présentationnelle) : nombre d'orphelines ou « aucune », une ligne par orpheline (nom, motif traduit, `CategoryCombobox` du nouveau parent alimenté par `buildParentOptions` avec l'orpheline en catégorie éditée, « Racine » présélectionnée en `extraOptions`, bouton « Rattacher »), bouton « Fermer ». La liste du combobox sort de la fenêtre par son portail (#429). Après un rattachement, une ligne `logInfo` donne le motif et le nombre de catégories remises en place, sans nom de catégorie, puis la liste et l'arbre se rechargent.
- **Fenêtre** — `HierarchyCheckModal` (présentationnelle) : nombre d'orphelines ou « aucune », une ligne par orpheline (nom, motif traduit, `CategoryCombobox` du nouveau parent alimenté par `buildParentOptions` avec l'orpheline en catégorie éditée, « Racine » présélectionnée en `extraOptions`, bouton « Rattacher »), bouton « Fermer ». La liste du combobox sort de la fenêtre par son portail (#429). À l'ouverture, `checkHierarchy` relit les catégories avant le diagnostic, sans l'état de chargement de la page (un formulaire ouvert derrière la fenêtre reste tel quel) : un glisser-déposer laisse dans `state.categories` ce que montre l'arbre (`SET_TREE`), sans les catégories qu'aucune racine n'atteint, que la fenêtre doit nommer et dont elle calcule les parents proposés. Après un rattachement, une ligne `logInfo` donne le motif et le nombre de catégories remises en place, sans nom de catégorie, puis la liste et l'arbre se rechargent.
- **Budget** — `buildBudgetYearRows` (`useBudget.ts`) et `getBudgetVsActualData` (`budgetService.ts`) placent au premier niveau une catégorie dont le parent n'est pas dans la liste active (`displaysAsRoot`), comme l'arbre et les sélecteurs ; une boucle y reste cachée, la fenêtre la répare.
- **Hors portée** — le glisser-déposer : `updateCategorySortOrders` écrit `parent_id` sans cette validation, gardé par l'interface (`moveCategory` ne peut pas déposer une catégorie dans son propre sous-arbre) ; il ne refuse pas non plus un parent qui porte des transactions, des montants de budget ou des mots-clés, et ne touche pas `is_inputable`. La restauration d'une sauvegarde (`restoreCategories`) réinsère les `parent_id` tels quels, boucles comprises : la fenêtre répare après coup.