fix(i18n): say « become a parent » precisely, and what to do with budget amounts (#393)
Decision of Max of 2026-10-20 and review 25921, point 3. The #393 changelog entries, the guide tip and docs.categories.tips (FR and EN) now say a category that still takes input cannot become a parent, and that an existing parent, no longer inputable, accepts new subcategories as before. The parent_has_budget message and the tip say amounts of any year count, to be set to zero in the grid year by year, and that a template cannot be edited, only deleted; the tip adds that zeroing a past year also takes its planned amounts out of Budget vs Actual. architecture.md: the rule and its reason, the hierarchy rows now carrying is_inputable, readCategoriesAgain after a reattachment (a form open on the orphan itself keeps its old parent: known limit), and the hook harness users presented as a count with the command that lists them (21 on 2026-10-20), not as an exhaustive list (25921, point 6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc6610103b
commit
b626bcbabe
7 changed files with 16 additions and 15 deletions
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|
||||||
- Catégories : un nouveau parent est désormais vérifié à l'enregistrement d'une catégorie. L'une de ses propres sous-catégories (la boucle de parenté qui cachait toute une branche) et un parent qui créerait un quatrième niveau sont refusés avec un message, et rien n'est enregistré. Un parent qui a des transactions, un montant de budget non nul (dans le budget ou dans un modèle de budget) ou des mots-clés à son nom est aussi refusé, puisque devenu parent il ne serait plus saisissable : à l'enregistrement d'une catégorie comme à la création d'une sous-catégorie ; le message dit quoi déplacer d'abord. Une catégorie enregistrée sans changement de parent n'est jamais refusée par ces vérifications (#393).
|
- Catégories : un nouveau parent est désormais vérifié à l'enregistrement d'une catégorie. L'une de ses propres sous-catégories (la boucle de parenté qui cachait toute une branche) et un parent qui créerait un quatrième niveau sont refusés avec un message, et rien n'est enregistré. Une catégorie encore saisissable ne peut pas devenir parent tant qu'elle a des transactions, un montant de budget non nul (d'une année quelconque, ou dans un modèle de budget) ou des mots-clés à son nom, puisqu'elle ne serait plus saisissable : la vérification se fait à l'enregistrement d'une catégorie comme à la création d'une sous-catégorie, et le message dit quoi déplacer d'abord. Un parent existant, qui n'est plus saisissable, accepte de nouvelles sous-catégories comme avant. Une catégorie enregistrée sans changement de parent n'est jamais refusée par ces vérifications (#393).
|
||||||
- Budget : une catégorie dont le parent a été supprimé ou est introuvable ne disparaît plus de la grille Budget ni du rapport Budget vs réel : elle s'affiche au premier niveau, comme dans l'arbre des catégories (#393).
|
- Budget : une catégorie dont le parent a été supprimé ou est introuvable ne disparaît plus de la grille Budget ni du rapport Budget vs réel : elle s'affiche au premier niveau, comme dans l'arbre des catégories (#393).
|
||||||
- Import : les messages de « Créer un dossier d'exemple » suivent désormais la langue de l'application : un dossier d'exemple déjà présent dans le dossier d'import (nommé, avec la marche à suivre : actualiser la liste, ou le renommer s'il n'y apparaît pas), un dossier d'import introuvable, une date du jour illisible. Toute autre erreur reste affichée telle que le système l'a écrite. Une fois les dossiers d'exemple écrits, le message de réussite ne s'affiche que si le dossier d'import a pu être relu ; sinon le message d'erreur s'affiche seul, au lieu des deux à la fois (#384).
|
- Import : les messages de « Créer un dossier d'exemple » suivent désormais la langue de l'application : un dossier d'exemple déjà présent dans le dossier d'import (nommé, avec la marche à suivre : actualiser la liste, ou le renommer s'il n'y apparaît pas), un dossier d'import introuvable, une date du jour illisible. Toute autre erreur reste affichée telle que le système l'a écrite. Une fois les dossiers d'exemple écrits, le message de réussite ne s'affiche que si le dossier d'import a pu être relu ; sinon le message d'erreur s'affiche seul, au lieu des deux à la fois (#384).
|
||||||
- Import : après le choix d'un autre dossier d'import, l'avertissement des fichiers posés directement dans le dossier d'import ne nomme plus les fichiers de l'ancien dossier sous le nom du nouveau, pendant la lecture du nouveau dossier ou quand il ne peut pas être lu (#446).
|
- Import : après le choix d'un autre dossier d'import, l'avertissement des fichiers posés directement dans le dossier d'import ne nomme plus les fichiers de l'ancien dossier sous le nom du nouveau, pendant la lecture du nouveau dossier ou quand il ne peut pas être lu (#446).
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Categories: a new parent is now checked when a category is saved. One of the category's own subcategories (the parent loop that hid a whole branch) and a parent that would make a fourth level are refused with a message, and nothing is saved. A parent that holds transactions, a budget amount other than zero (in the budget or in a budget template) or keywords of its own is refused too, since once a parent it would no longer take input: when a category is saved, and when a subcategory is created under it; the message says what to move first. A category saved with its parent unchanged is never refused by these checks (#393).
|
- Categories: a new parent is now checked when a category is saved. One of the category's own subcategories (the parent loop that hid a whole branch) and a parent that would make a fourth level are refused with a message, and nothing is saved. A category that still takes input cannot become a parent while it holds transactions, a budget amount other than zero (of any year, or in a budget template) or keywords of its own, since it would no longer take input: this is checked when a category is saved and when a subcategory is created, and the message says what to move first. An existing parent, which no longer takes input, accepts new subcategories as before. A category saved with its parent unchanged is never refused by these checks (#393).
|
||||||
- Budget: a category whose parent was deleted or cannot be found no longer disappears from the Budget grid and the Budget vs Actual report: it now shows at the top level, as in the category tree (#393).
|
- Budget: a category whose parent was deleted or cannot be found no longer disappears from the Budget grid and the Budget vs Actual report: it now shows at the top level, as in the category tree (#393).
|
||||||
- Import: the messages of “Create a sample folder” now follow the app's language: a sample folder already in the import folder (named, with what to do: refresh the list, or rename it if it does not show up), an import folder that cannot be found, today's date unreadable. Any other error still shows as the system wrote it. Once the sample folders are written, the success message only shows when the import folder could be read again; otherwise the error message shows alone, instead of both at once (#384).
|
- Import: the messages of “Create a sample folder” now follow the app's language: a sample folder already in the import folder (named, with what to do: refresh the list, or rename it if it does not show up), an import folder that cannot be found, today's date unreadable. Any other error still shows as the system wrote it. Once the sample folders are written, the success message only shows when the import folder could be read again; otherwise the error message shows alone, instead of both at once (#384).
|
||||||
- Import: after you choose another import folder, the warning about the files placed directly in the import folder no longer names the previous folder's files under the new folder's name, while the new folder is being read or when it cannot be read (#446).
|
- Import: after you choose another import folder, the warning about the files placed directly in the import folder no longer names the previous folder's files under the new folder's name, while the new folder is being read or when it cannot be read (#446).
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ Le dossier des bases est celui du plugin : `app_config_dir()`, résolu par `prof
|
||||||
|---------|---------------|
|
|---------|---------------|
|
||||||
| `db.ts` | Wrapper de connexion (tauri-plugin-sql), verrou FIFO de tous les accès et `withTransaction` (voir « Accès DB et transactions »). `connectToProfile` appelle `repair_migrations` puis `prepare_profile_db` avant `Database.load`, et écrit au Journal ce que la préparation a changé (`describePreparation`, noms de fichiers seulement, #390) ; un échec est écrit au Journal et le profil s'ouvre quand même |
|
| `db.ts` | Wrapper de connexion (tauri-plugin-sql), verrou FIFO de tous les accès et `withTransaction` (voir « Accès DB et transactions »). `connectToProfile` appelle `repair_migrations` puis `prepare_profile_db` avant `Database.load`, et écrit au Journal ce que la préparation a changé (`describePreparation`, noms de fichiers seulement, #390) ; un échec est écrit au Journal et le profil s'ouvre quand même |
|
||||||
| `profileService.ts` | Gestion des profils |
|
| `profileService.ts` | Gestion des profils |
|
||||||
| `categoryService.ts` | CRUD catégories hiérarchiques. `updateCategory` vérifie un nouveau parent, `reattachCategory` rattache une catégorie orpheline et `createCategory` vérifie ce que porte le parent d'une sous-catégorie, chacun en une transaction par son handle ; `CategoryHierarchyError` typée ; `findOrphanCategories` (diagnostic, requête dédiée sur toutes les catégories) ; `getCategoryDepth` s'arrête sur un cycle — voir « Hiérarchie des catégories » |
|
| `categoryService.ts` | CRUD catégories hiérarchiques. `updateCategory` vérifie un nouveau parent, `reattachCategory` rattache une catégorie orpheline et `createCategory` vérifie ce que porte le parent d'une sous-catégorie quand il deviendrait parent, chacun en une transaction par son handle ; `CategoryHierarchyError` typée ; `findOrphanCategories` (diagnostic, requête dédiée sur toutes les catégories) ; `getCategoryDepth` s'arrête sur un cycle — voir « Hiérarchie des catégories » |
|
||||||
| `transactionService.ts` | CRUD et filtrage des transactions ; détection d'erreurs FK RESTRICT pour transactions liées à un compte de bilan (typed `TransactionLinkedToBalanceError`) ; `getTransactionCount` (compte brut de la table, sans filtre — signal des états vides de Transactions et Budget, #351/#353) |
|
| `transactionService.ts` | CRUD et filtrage des transactions ; détection d'erreurs FK RESTRICT pour transactions liées à un compte de bilan (typed `TransactionLinkedToBalanceError`) ; `getTransactionCount` (compte brut de la table, sans filtre — signal des états vides de Transactions et Budget, #351/#353) |
|
||||||
| `importSourceService.ts` | Configuration des sources d'import — lit et écrit le format complet via le codec `importFormat.ts` ([ADR 0019](adr/0019-format-import-persiste.md)) |
|
| `importSourceService.ts` | Configuration des sources d'import — lit et écrit le format complet via le codec `importFormat.ts` ([ADR 0019](adr/0019-format-import-persiste.md)) |
|
||||||
| `importedFileService.ts` | Suivi des fichiers importés |
|
| `importedFileService.ts` | Suivi des fichiers importés |
|
||||||
|
|
@ -238,7 +238,7 @@ Les 29 fichiers `.ts` de `src/hooks/`, hors tests (`ls src/hooks/*.ts | grep -v
|
||||||
|
|
||||||
| Hook | Domaine |
|
| Hook | Domaine |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `useCategories` | Catégories avec hiérarchie. Erreurs typées `CategoriesError` (`{ key }` d'un refus de la hiérarchie, `{ message }` sinon, `categoriesErrorOf`). Fenêtre « Vérifier la hiérarchie » : `hierarchyCheck`, `checkHierarchy`, `reattachOrphan` (une ligne de Journal, puis rechargement de la liste et de l'arbre), `closeHierarchyCheck` (#393) |
|
| `useCategories` | Catégories avec hiérarchie. Erreurs typées `CategoriesError` (`{ key }` d'un refus de la hiérarchie, `{ message }` sinon, `categoriesErrorOf`). Fenêtre « Vérifier la hiérarchie » : `hierarchyCheck`, `checkHierarchy`, `reattachOrphan` (une ligne de Journal, puis relecture de la liste et des catégories), `closeHierarchyCheck` ; relecture des catégories sans état de chargement (`readCategoriesAgain`) (#393) |
|
||||||
| `useCategoryMigration` | Migration vers la taxonomie standard (`/settings/categories/migrate`, `CategoriesMigrationPage`) : machine d'états des étapes découvrir, simuler et consentir, puis exécution et résultat. Reducer sans appel de service : la page calcule le plan, écrit la sauvegarde préalable (SREF) et applique la migration, puis dispatche |
|
| `useCategoryMigration` | Migration vers la taxonomie standard (`/settings/categories/migrate`, `CategoriesMigrationPage`) : machine d'états des étapes découvrir, simuler et consentir, puis exécution et résultat. Reducer sans appel de service : la page calcule le plan, écrit la sauvegarde préalable (SREF) et applique la migration, puis dispatche |
|
||||||
| `useCategoryTaxonomy` | Taxonomie standard v1 en lecture (`getTaxonomyV1`, `findById`, `findByPath`, `getLeaves`, `getParentById` de `categoryTaxonomyService`), mémorisée une fois : guide des catégories standard (`CategoriesStandardGuidePage`) et étapes de la migration |
|
| `useCategoryTaxonomy` | Taxonomie standard v1 en lecture (`getTaxonomyV1`, `findById`, `findByPath`, `getLeaves`, `getParentById` de `categoryTaxonomyService`), mémorisée une fois : guide des catégories standard (`CategoriesStandardGuidePage`) et étapes de la migration |
|
||||||
| `useTransactions` | Transactions et filtrage. `useTransactions(initialFilters?)` part de filtres de départ posés sur les filtres par défaut et lus au montage seulement (`initTransactionsState`) : l'entrée guidée de l'étape « Classer une transaction à la main » passe `GUIDED_ENTRY_FILTERS` (non catégorisées, sans borne de date). Si sa première page revient vide, le reducer retire le filtre « non catégorisées » une seule fois (`guidedFallback`) et la page relit (#399) |
|
| `useTransactions` | Transactions et filtrage. `useTransactions(initialFilters?)` part de filtres de départ posés sur les filtres par défaut et lus au montage seulement (`initTransactionsState`) : l'entrée guidée de l'étape « Classer une transaction à la main » passe `GUIDED_ENTRY_FILTERS` (non catégorisées, sans borne de date). Si sa première page revient vide, le reducer retire le filtre « non catégorisées » une seule fois (`guidedFallback`) et la page relit (#399) |
|
||||||
|
|
@ -280,10 +280,10 @@ Les 29 fichiers `.ts` de `src/hooks/`, hors tests (`ls src/hooks/*.ts | grep -v
|
||||||
Un `parent_id` en boucle (A sous B, B sous A, ou une catégorie sous elle-même) cache la boucle et tout ce qui pend dessous partout où l'on part des racines : arbre, sélecteurs de catégorie, budget, rapports. Les données restent intactes ; seul `parent_id` est en cause.
|
Un `parent_id` en boucle (A sous B, B sous A, ou une catégorie sous elle-même) cache la boucle et tout ce qui pend dessous partout où l'on part des racines : arbre, sélecteurs de catégorie, budget, rapports. Les données restent intactes ; seul `parent_id` est en cause.
|
||||||
|
|
||||||
- **Module pur unique** — `src/utils/categoryHierarchy.ts` : `depthsOf` (une catégorie prise dans une boucle, ou dessous, n'a **pas** de profondeur), `subtreeHeight` et `descendantsOf` (une seule descente, `levelsBelow`), `fitsUnder` (trois niveaux au plus, `MAX_CATEGORY_DEPTH = 2`, sous-arbre compris), `parentChangeRefusal`, `findOrphans`, `displaysAsRoot`. Chaque remontée et chaque descente garde l'ensemble des catégories vues : une boucle termine le parcours. Les parcours ont trois consommateurs, sans autre implémentation : `buildParentOptions` (liste des parents du formulaire, #423, et de la fenêtre), le diagnostic et la validation du service ; le budget lit `displaysAsRoot`. `computeDepths` de `CategoryCombobox` ne sert qu'à l'indentation et reste à part.
|
- **Module pur unique** — `src/utils/categoryHierarchy.ts` : `depthsOf` (une catégorie prise dans une boucle, ou dessous, n'a **pas** de profondeur), `subtreeHeight` et `descendantsOf` (une seule descente, `levelsBelow`), `fitsUnder` (trois niveaux au plus, `MAX_CATEGORY_DEPTH = 2`, sous-arbre compris), `parentChangeRefusal`, `findOrphans`, `displaysAsRoot`. Chaque remontée et chaque descente garde l'ensemble des catégories vues : une boucle termine le parcours. Les parcours ont trois consommateurs, sans autre implémentation : `buildParentOptions` (liste des parents du formulaire, #423, et de la fenêtre), le diagnostic et la validation du service ; le budget lit `displaysAsRoot`. `computeDepths` de `CategoryCombobox` ne sert qu'à l'indentation et reste à part.
|
||||||
- **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.
|
- **Diagnostic** — `findOrphanCategories` lit **toutes** les catégories, actives ou non (`SELECT id, parent_id, is_active, is_inputable FROM categories`, la lecture de la validation) : 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.
|
- **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, négatif compris, d'une année quelconque dans `budget_entries` ou dans `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 ne se vérifie que si le parent est encore saisissable (`is_inputable = 1`, la condition de `markAsParent`, lue dans les lignes de la hiérarchie) : c'est alors qu'il deviendrait parent et cesserait d'être saisissable. Un parent existant, déjà non saisissable, accepte un enfant quoi qu'il porte, comme dans toutes les versions publiées : le refus n'y protégerait rien (décision de Max du 2026-10-20, revue 25921). La règle vaut pour les trois codes, `parent_has_transactions` compris, et pour les trois points d'entrée. `createCategory` vérifie de même le parent d'une sous-catégorie, sur les lignes de la hiérarchie lues par le handle, 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.
|
- **`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). À l'ouverture, `checkHierarchy` relit les catégories avant le diagnostic, une fois finies les écritures d'un glisser-déposer en cours (un `UPDATE` par catégorie, qu'une lecture à mi-chemin montrerait à moitié appliqué), 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.
|
- **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 (`readCategoriesAgain`) avant le diagnostic, une fois finies les écritures d'un glisser-déposer en cours (un `UPDATE` par catégorie, qu'une lecture à mi-chemin montrerait à moitié appliqué), 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 les catégories sont relues de la même façon : un formulaire ouvert derrière la fenêtre garde ses changements non enregistrés (jusqu'à la revue 25921, `loadCategories` les effaçait), et un échec de cette relecture s'affiche dans la fenêtre. Limite connue : un formulaire ouvert sur l'orpheline elle-même garde son ancien parent, qu'un enregistrement remettrait (sous les mêmes vérifications).
|
||||||
- **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.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|
@ -661,7 +661,7 @@ Sémantique simulée, chaque point prouvé par `src/test/hookHarness.test.ts` :
|
||||||
|
|
||||||
Non simulé : le nouveau rendu après un setter ou un dispatch, les enfants (un composant dans l'arbre rendu reste un élément), le DOM, le concurrent, la boucle de mises à jour pendant le rendu (le `render()` suivant la lit), et un reducer qui change d'un rendu à l'autre : le `dispatch` d'un `useReducer` garde le reducer du premier rendu (il est créé une fois, avec l'emplacement), là où React applique celui du dernier rendu. Un reducer défini au niveau du module, seul cas utilisé, n'est pas touché.
|
Non simulé : le nouveau rendu après un setter ou un dispatch, les enfants (un composant dans l'arbre rendu reste un élément), le DOM, le concurrent, la boucle de mises à jour pendant le rendu (le `render()` suivant la lit), et un reducer qui change d'un rendu à l'autre : le `dispatch` d'un `useReducer` garde le reducer du premier rendu (il est créé une fois, avec l'emplacement), là où React applique celui du dernier rendu. Un reducer défini au niveau du module, seul cas utilisé, n'est pas touché.
|
||||||
|
|
||||||
Utilisateurs : `useOnboardingChecklist.wiring.test.ts`, `UsersSettingsPage.test.tsx`, `BudgetTable.wiring.test.ts`, `BudgetPage.wiring.test.ts`, `AppShell.slot.test.tsx`, `OnboardingStepCard.test.tsx`, `PriceFetchConsentToggle.test.tsx` (#389), `PriceFetchControl.test.tsx` (#437). Hors périmètre de #409, `useImportWizard.mount.test.ts` et `useImportWizard.executeImport.test.ts` gardent leurs faux non persistants ; `useImportWizard.samples.test.ts` (« Créer un dossier d'exemple », #384) passe par le harnais partagé.
|
Utilisateurs : 21 fichiers de test au 2026-10-20, que `grep -rl "test/hookHarness" src` liste ; d'abord ceux de #389 (`useOnboardingChecklist.wiring.test.ts`, `UsersSettingsPage.test.tsx`, `BudgetTable.wiring.test.ts`, `BudgetPage.wiring.test.ts`, `AppShell.slot.test.tsx`, `OnboardingStepCard.test.tsx`, `PriceFetchConsentToggle.test.tsx`), puis les tests de câblage et de hooks venus après (`PriceFetchControl.test.tsx` en #437, `TransactionTable.wiring.test.ts`, `useCategories.test.ts`…). `CategoryCombobox.placement.test.ts` y fait passer `useLayoutEffect` pour `useEffect` et `useId` pour une constante, dans ce seul fichier (#429). Hors périmètre de #409, `useImportWizard.mount.test.ts` et `useImportWizard.executeImport.test.ts` gardent leurs faux non persistants ; `useImportWizard.samples.test.ts` (« Créer un dossier d'exemple », #384) passe par le harnais partagé.
|
||||||
|
|
||||||
### Faux `t` marqué — `src/test/i18nMock.ts` (#389)
|
### Faux `t` marqué — `src/test/i18nMock.ts` (#389)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ Gérez votre arbre de catégories avec des sous-catégories, des règles de mots
|
||||||
- Utilisez la vue Tous les mots-clés pour avoir un aperçu global de vos règles de catégorisation
|
- Utilisez la vue Tous les mots-clés pour avoir un aperçu global de vos règles de catégorisation
|
||||||
- Utilisez Réinitialiser pour revenir aux catégories par défaut — cela dissociera toutes les catégories des transactions
|
- Utilisez Réinitialiser pour revenir aux catégories par défaut — cela dissociera toutes les catégories des transactions
|
||||||
- Une catégorie disparue de l'arbre, des menus et du budget est souvent prise dans une boucle de parenté : Vérifier la hiérarchie la retrouve, et ses transactions, ses mots-clés et ses montants de budget reviennent avec elle
|
- Une catégorie disparue de l'arbre, des menus et du budget est souvent prise dans une boucle de parenté : Vérifier la hiérarchie la retrouve, et ses transactions, ses mots-clés et ses montants de budget reviennent avec elle
|
||||||
- Une catégorie qui a des transactions, des montants de budget (dans le budget ou dans un modèle) ou des mots-clés à son nom ne peut pas devenir un parent, ni d'une catégorie existante ni d'une nouvelle sous-catégorie, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, remettez ses montants à zéro ou supprimez ses mots-clés, ou choisissez un autre parent
|
- Une catégorie encore saisissable qui a des transactions, des montants de budget (d'une année quelconque, ou dans un modèle) ou des mots-clés à son nom ne peut pas devenir parent, ni d'une catégorie existante ni d'une nouvelle sous-catégorie, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, remettez ses montants à zéro dans la grille du Budget pour chaque année concernée (pour une année passée, son prévu quitte aussi le rapport Budget vs Réel) et supprimez les modèles qui en contiennent, un modèle ne se modifiant pas, supprimez ses mots-clés, ou choisissez un autre parent. Un parent existant, qui n'est plus saisissable, accepte de nouvelles sous-catégories sans cette vérification
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ const ROOT = "";
|
||||||
* row per group to reattach, each with its reason and a new parent. The root
|
* row per group to reattach, each with its reason and a new parent. The root
|
||||||
* is preselected; the other choices are those of the category form
|
* is preselected; the other choices are those of the category form
|
||||||
* (`buildParentOptions`, the row's category treated as the one being edited).
|
* (`buildParentOptions`, the row's category treated as the one being edited).
|
||||||
* The service applies the rules again and may refuse (a parent that holds
|
* The service applies the rules again and may refuse (a parent that still
|
||||||
* transactions, for one): the translated refusal shows above the list.
|
* takes input and holds transactions, for one): the translated refusal shows
|
||||||
|
* above the list.
|
||||||
*
|
*
|
||||||
* The comboboxes open their list in a portal (#429), so the scrolling list of
|
* The comboboxes open their list in a portal (#429), so the scrolling list of
|
||||||
* rows never cuts it.
|
* rows never cuts it.
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@
|
||||||
"cycle": "This parent would create a parent loop: it sits under this category, or inside a loop. Choose another parent.",
|
"cycle": "This parent would create a parent loop: it sits under this category, or inside a loop. Choose another parent.",
|
||||||
"too_deep": "With this parent, the category or its subcategories would go beyond three levels. Choose a parent higher in the hierarchy.",
|
"too_deep": "With this parent, the category or its subcategories would go beyond three levels. Choose a parent higher in the hierarchy.",
|
||||||
"parent_has_transactions": "This parent has transactions of its own: once a parent, it would no longer take input. Choose another parent, or recategorize its transactions first.",
|
"parent_has_transactions": "This parent has transactions of its own: once a parent, it would no longer take input. Choose another parent, or recategorize its transactions first.",
|
||||||
"parent_has_budget": "This parent has budget amounts of its own, in the budget or in a budget template: once a parent, it would no longer take input and those amounts would drop out of the grid. Choose another parent, or first set those amounts to zero and delete, on the Budget page, the templates that hold any.",
|
"parent_has_budget": "This parent has budget amounts of its own, for any year in the Budget grid or in a budget template: once a parent, it would no longer take input and those amounts would drop out of the grid. Choose another parent, or first set its amounts to zero in the grid, for every year that has some, and delete, on the Budget page, the templates that hold any: a template cannot be edited.",
|
||||||
"parent_has_keywords": "This parent has keywords of its own: once a parent, it would no longer take input, yet its keywords would keep sorting transactions into it. Choose another parent, or first delete its keywords (add them to another category if needed)."
|
"parent_has_keywords": "This parent has keywords of its own: once a parent, it would no longer take input, yet its keywords would keep sorting transactions into it. Choose another parent, or first delete its keywords (add them to another category if needed)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1203,7 +1203,7 @@
|
||||||
"Use the All Keywords view to get a global overview of your categorization rules",
|
"Use the All Keywords view to get a global overview of your categorization rules",
|
||||||
"Use Re-initialize to reset categories to defaults — this will unlink all transaction categories",
|
"Use Re-initialize to reset categories to defaults — this will unlink all transaction categories",
|
||||||
"A category missing from the tree, the menus and the budget is often caught in a parent loop: Check hierarchy finds it, and its transactions, keywords and budget amounts come back with it",
|
"A category missing from the tree, the menus and the budget is often caught in a parent loop: Check hierarchy finds it, and its transactions, keywords and budget amounts come back with it",
|
||||||
"A category with transactions, budget amounts (in the budget or in a template) or keywords of its own cannot become a parent, of an existing category or of a new subcategory, since it would no longer take input: first recategorize its transactions, set its amounts to zero or delete its keywords, or choose another parent"
|
"A category that still takes input and has transactions, budget amounts (of any year, or in a template) or keywords of its own cannot become a parent, of an existing category or of a new subcategory, since it would no longer take input: first recategorize its transactions, set its amounts to zero in the Budget grid for every year concerned (for a past year, its planned amounts also leave the Budget vs Actual report) and delete the templates that hold any, since a template cannot be edited, delete its keywords, or choose another parent. An existing parent, which no longer takes input, accepts new subcategories without this check"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"adjustments": {
|
"adjustments": {
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@
|
||||||
"cycle": "Ce parent créerait une boucle de parenté : il se trouve sous cette catégorie, ou dans une boucle. Choisissez un autre parent.",
|
"cycle": "Ce parent créerait une boucle de parenté : il se trouve sous cette catégorie, ou dans une boucle. Choisissez un autre parent.",
|
||||||
"too_deep": "Avec ce parent, la catégorie ou ses sous-catégories dépasseraient trois niveaux. Choisissez un parent plus haut dans la hiérarchie.",
|
"too_deep": "Avec ce parent, la catégorie ou ses sous-catégories dépasseraient trois niveaux. Choisissez un parent plus haut dans la hiérarchie.",
|
||||||
"parent_has_transactions": "Ce parent a des transactions à son nom : devenu parent, il ne serait plus saisissable. Choisissez un autre parent, ou reclassez d'abord ses transactions.",
|
"parent_has_transactions": "Ce parent a des transactions à son nom : devenu parent, il ne serait plus saisissable. Choisissez un autre parent, ou reclassez d'abord ses transactions.",
|
||||||
"parent_has_budget": "Ce parent a des montants de budget à son nom, dans le budget ou dans un modèle de budget : devenu parent, il ne serait plus saisissable et ces montants sortiraient de la grille. Choisissez un autre parent, ou remettez d'abord ces montants à zéro et supprimez, dans la page Budget, les modèles qui en contiennent.",
|
"parent_has_budget": "Ce parent a des montants de budget à son nom, d'une année quelconque dans la grille du Budget ou dans un modèle de budget : devenu parent, il ne serait plus saisissable et ces montants sortiraient de la grille. Choisissez un autre parent, ou remettez d'abord ses montants à zéro dans la grille, pour chaque année qui en a, et supprimez, dans la page Budget, les modèles qui en contiennent : un modèle ne se modifie pas.",
|
||||||
"parent_has_keywords": "Ce parent a des mots-clés à son nom : devenu parent, il ne serait plus saisissable, mais ses mots-clés continueraient d'y classer des transactions. Choisissez un autre parent, ou supprimez d'abord ses mots-clés (ajoutez-les au besoin à une autre catégorie)."
|
"parent_has_keywords": "Ce parent a des mots-clés à son nom : devenu parent, il ne serait plus saisissable, mais ses mots-clés continueraient d'y classer des transactions. Choisissez un autre parent, ou supprimez d'abord ses mots-clés (ajoutez-les au besoin à une autre catégorie)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1203,7 +1203,7 @@
|
||||||
"Utilisez la vue Tous les mots-clés pour avoir un aperçu global de vos règles de catégorisation",
|
"Utilisez la vue Tous les mots-clés pour avoir un aperçu global de vos règles de catégorisation",
|
||||||
"Utilisez Réinitialiser pour revenir aux catégories par défaut — cela dissociera toutes les catégories des transactions",
|
"Utilisez Réinitialiser pour revenir aux catégories par défaut — cela dissociera toutes les catégories des transactions",
|
||||||
"Une catégorie disparue de l'arbre, des menus et du budget est souvent prise dans une boucle de parenté : Vérifier la hiérarchie la retrouve, et ses transactions, ses mots-clés et ses montants de budget reviennent avec elle",
|
"Une catégorie disparue de l'arbre, des menus et du budget est souvent prise dans une boucle de parenté : Vérifier la hiérarchie la retrouve, et ses transactions, ses mots-clés et ses montants de budget reviennent avec elle",
|
||||||
"Une catégorie qui a des transactions, des montants de budget (dans le budget ou dans un modèle) ou des mots-clés à son nom ne peut pas devenir un parent, ni d'une catégorie existante ni d'une nouvelle sous-catégorie, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, remettez ses montants à zéro ou supprimez ses mots-clés, ou choisissez un autre parent"
|
"Une catégorie encore saisissable qui a des transactions, des montants de budget (d'une année quelconque, ou dans un modèle) ou des mots-clés à son nom ne peut pas devenir parent, ni d'une catégorie existante ni d'une nouvelle sous-catégorie, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, remettez ses montants à zéro dans la grille du Budget pour chaque année concernée (pour une année passée, son prévu quitte aussi le rapport Budget vs Réel) et supprimez les modèles qui en contiennent, un modèle ne se modifiant pas, supprimez ses mots-clés, ou choisissez un autre parent. Un parent existant, qui n'est plus saisissable, accepte de nouvelles sous-catégories sans cette vérification"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"adjustments": {
|
"adjustments": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue