diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index d2b014c..492dc32 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,11 +2,20 @@ ## [Non publié] +### Ajouté + +- Catégories : un bouton « Vérifier la hiérarchie », entre « Tous les mots-clés » et « Réinitialiser », liste les catégories orphelines : celles prises dans une boucle de parenté, qui les cache avec leurs sous-catégories de l'arbre, des menus de catégories, du budget et des rapports, et celles dont le parent a été supprimé ou est introuvable. Chacune se rattache à la racine, choisie par défaut, ou à un autre parent ; elle revient alors à sa place avec ses transactions, ses mots-clés et ses montants de budget. Le Journal note chaque rattachement, sans nom de catégorie (#393). + ### Modifié - Paramètres → Système : « Journaux et commentaires » vient désormais juste après « Mise à jour », avant l'historique des versions, qui ne repousse plus le Journal en bas de la page. L'historique des versions s'ouvre sur les notes de la version installée, les autres versions repliées : cliquez sur un numéro de version pour lire ses notes, ou sur « Tout déplier » pour ouvrir tout l'historique (#372). - Ajustements : la catégorie d'une ligne d'ajustement accepte désormais la saisie, comme la cellule de catégorie de la page Transactions : tapez quelques lettres pour filtrer la liste, qui montre la hiérarchie et les noms des catégories standard dans votre langue. Elle propose les mêmes catégories que la page Transactions, actives et autorisées à la saisie, comme la fenêtre de répartition ouverte depuis la page Ajustements ; une ligne déjà enregistrée sur une catégorie masquée de la saisie la garde, son champ vide tant que vous n'en choisissez pas une autre. Le filtre de catégorie de la fenêtre « Lier transferts » du Bilan accepte aussi la saisie, avec « Toutes les catégories » en tête. Une liste de catégories ouverte n'est plus coupée par le bord d'une fenêtre ou du tableau des transactions, dans la fenêtre de répartition comme sur les dernières lignes du tableau, et elle s'ouvre vers le haut quand elle ne tient pas en dessous mais tient au-dessus (#429). +### 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), un parent qui créerait un quatrième niveau et un parent qui a des transactions à son nom (devenu parent, il ne serait plus saisissable) sont refusés avec un message, et rien n'est enregistré. 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). + ## [0.17.2] - 2026-10-18 ### Ajouté diff --git a/CHANGELOG.md b/CHANGELOG.md index 1962e74..fe19004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,20 @@ ## [Unreleased] +### Added + +- Categories: a “Check hierarchy” button, between “All keywords” and “Re-initialize”, lists the orphan categories: those caught in a parent loop, which hides them and their subcategories from the tree, the category menus, the budget and the reports, and those whose parent was deleted or cannot be found. Each one can be reattached to the root, chosen by default, or to another parent; it then comes back in its place with its transactions, keywords and budget amounts. The Journal notes each reattachment, without any category name (#393). + ### Changed - Settings → System: “Logs and feedback” now comes right after “Update”, before the version history, which no longer pushes the Journal to the bottom of the page. The version history opens on the notes of the installed version, with every other version folded: click a version number to read its notes, or “Expand all” to open the whole history (#372). - Adjustments: the category of an adjustment line now takes typing, like the category cell of the Transactions page: type a few letters to filter the list, which shows the hierarchy and the names of the standard categories in your language. It offers the same categories as the Transactions page, active and allowed for input, as does the split window opened from the Adjustments page; a line already saved on a category hidden from input keeps it, its field empty until you choose another one. The category filter of the “Link transfers” window of the Balance sheet takes typing too, with “Any category” first. An open category list is no longer cut by the edge of a window or of the Transactions table, in the split window as on the last rows of the table, and it opens upward when it does not fit below but fits above (#429). +### 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), a parent that would make a fourth level, and a parent with transactions of its own (once a parent, it would no longer take input) are refused with a message, and nothing is saved. 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). + ## [0.17.2] - 2026-10-18 ### Added diff --git a/docs/architecture.md b/docs/architecture.md index a41bd94..c3b7bc7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,7 @@ simpl-resultat/ │ ├── pages/ # 14 pages (dont 4 sous-pages rapports) │ ├── services/ # 27 services métier (21 détaillés dans la section « Services TypeScript ») │ ├── shared/ # Types, constantes, matrice d'entitlements (entitlements.ts), gate profils (profileGate.ts), gate de la Répartition (splitGate.ts), CTA d'upsell (upsellCta.ts), URL d'achat (purchaseUrl.ts), échéance de licence (licenseExpiry.ts) -│ ├── utils/ # 19 utilitaires purs (parsing montants/dates, détection CSV, codec de format d'import, dictionnaire lexical, signatures de banques, charts, drapeaux SQL 0/1, liste et parcours de démarrage, historique des versions) +│ ├── utils/ # 20 utilitaires purs (parsing montants/dates, détection CSV, codec de format d'import, dictionnaire lexical, signatures de banques, charts, drapeaux SQL 0/1, liste et parcours de démarrage, historique des versions, hiérarchie des catégories) │ ├── i18n/ # Config i18next + locales FR/EN │ ├── __fixtures__/ # csv/ : corpus de détection (15 cas figés) ; samples/ : gabarits du dossier d'exemple embarqués par Rust │ ├── App.tsx # Router principal @@ -196,7 +196,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 | | `profileService.ts` | Gestion des profils | -| `categoryService.ts` | CRUD catégories hiérarchiques | +| `categoryService.ts` | CRUD catégories hiérarchiques. `updateCategory` vérifie un nouveau parent et `reattachCategory` rattache une catégorie orpheline, 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) | | `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 | @@ -233,7 +233,7 @@ Chaque hook encapsule la logique d'état via `useReducer` : | Hook | Domaine | |------|---------| -| `useCategories` | Catégories avec hiérarchie | +| `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) | | `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) | | `useDataImport` | Import de données | | `useImportWizard` | Assistant d'import multi-étapes. Restaure le format enregistré **tel quel** via `formatFromRow` (aucune ré-inférence), déclenche la détection seule sur une source sans format (garde `!existing`), traverse l'étape `file-preview` à **chaque** import, et n'écrit la configuration qu'à `executeImport` (point d'écriture unique) — voir la section « Import CSV » et l'[ADR 0019](adr/0019-format-import-persiste.md). Expose `importFolderLoaded`, posé dans le `finally` de la lecture du dossier au montage (après son premier scan) et gardé par `RESET` : l'état vide de la page Import et sa carte de démarrage l'attendent (#384, #398). Expose aussi `rootFiles`, les CSV/TXT posés à la racine du dossier d'import que le scan (`ImportFolderScan`) rend à côté des sources : posé avec elles par la même action `SET_SCANNED_SOURCES`, gardé par `RESET` comme elles, nommé à l'écran par `RootFilesWarning` (au plus cinq noms, `formatRootFiles`) et jamais écrit au Journal (#427). `executeImport` enregistre la source sous `selectedSource.folder_name`, à la création comme à la mise à jour, jamais sous `config.name` : la source et ses fichiers importés sont retrouvés par le nom du sous-dossier, que `SourceNameDisplay` affiche en lecture seule (#426) | @@ -267,6 +267,18 @@ Chaque hook encapsule la logique d'état via `useReducer` : - `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). - L'historique des versions de Paramètres → Système (`ChangelogContent`, #372) n'utilise pas ce hook : c'est une liste plate dont une seule version s'ouvre au départ, choisie par une règle, alors que le hook n'a qu'un `defaultExpanded` global, sans amorce par clé. Son état est un `Set` local, en mémoire, amorcé à chaque lecture du fichier par `initiallyExpandedVersions` (`src/utils/changelog.ts`, pur, avec `parseChangelog`) : la version installée, ou la première version publiée quand la version installée est inconnue ou absente du fichier. +## Hiérarchie des catégories (#393) + +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. Trois consommateurs, aucune autre implémentation : `buildParentOptions` (liste des parents du formulaire, #423, et de la fenêtre), le diagnostic et la validation du service. `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. +- **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 `parent_has_transactions` pour un parent qui porte des transactions directes (devenu parent, il ne serait plus saisissable). 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.`. 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. +- **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 avec transactions 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. + ## Import CSV — format persisté et détection Le chantier #323-#332 a fait du **format d'import une donnée persistée intégralement, jamais re-devinée** — [ADR 0019](adr/0019-format-import-persiste.md) porte la décision, ses trois voies de perte et les alternatives rejetées. Quatre couches, du schéma à l'écran. @@ -565,7 +577,7 @@ Les modules payants sont enveloppés dans des **layout-routes pathless `RequireF | `/` | `DashboardPage` | Tableau de bord (KPIs+deltas, top movers, adhérence budget, tuile valeur nette, barres classées, dépenses dans le temps — modèle Cartes, #279) | | `/import` | `ImportPage` | Assistant d'import CSV : source → configuration (détection automatique + score) → sélection des fichiers → **aperçu obligatoire** (récap signé, inversion des signes, panneau de dérive) → doublons → confirmation | | `/transactions` | `TransactionsPage` | Liste avec filtres | -| `/categories` | `CategoriesPage` | Gestion hiérarchique | +| `/categories` | `CategoriesPage` | Gestion hiérarchique ; le bouton « Vérifier la hiérarchie » ouvre `HierarchyCheckModal` (#393) | | `/adjustments` | `AdjustmentsPage` | Ajustements manuels | | `/budget` | `BudgetPage` | Planification budgétaire | | `/reports` | `ReportsPage` | Hub des rapports : panneau faits saillants + 4 cartes de navigation | diff --git a/docs/guide-utilisateur.md b/docs/guide-utilisateur.md index 4dfa1d4..9819e1c 100644 --- a/docs/guide-utilisateur.md +++ b/docs/guide-utilisateur.md @@ -275,6 +275,7 @@ Gérez votre arbre de catégories avec des sous-catégories, des règles de mots - Glisser-déposer pour réordonner les catégories ou changer leur parent - Basculer les catégories en saisissable ou non-saisissable - Vue « Tous les mots-clés » pour voir l'ensemble des règles +- « Vérifier la hiérarchie » pour retrouver les catégories orphelines (prises dans une boucle de parenté, ou dont le parent a été supprimé ou est introuvable) et les rattacher - Réinitialiser les catégories aux valeurs par défaut ### Comment faire @@ -285,6 +286,7 @@ Gérez votre arbre de catégories avec des sous-catégories, des règles de mots 4. Ajoutez des mots-clés correspondant aux descriptions des transactions pour l'auto-catégorisation 5. Définissez la priorité des mots-clés pour résoudre les conflits quand plusieurs catégories correspondent 6. Utilisez le sélecteur de couleur pour assigner une couleur personnalisée pour les graphiques +7. Cliquez sur Vérifier la hiérarchie pour lister les catégories orphelines : pour chacune, choisissez un nouveau parent (la racine par défaut), puis cliquez sur Rattacher ### Astuces @@ -292,6 +294,8 @@ Gérez votre arbre de catégories avec des sous-catégories, des règles de mots - Les mots-clés de priorité supérieure l'emportent quand plusieurs catégories correspondent à la même transaction - 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 +- 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 à son nom ne peut pas devenir le parent d'une catégorie existante, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, ou choisissez un autre parent --- diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e31927a..1f31f84 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1178,6 +1178,7 @@ "Drag-and-drop to reorder categories or change their parent", "Toggle categories as inputable or non-inputable", "All Keywords view to see all rules at a glance", + "“Check hierarchy” to find the orphan categories (caught in a parent loop, or whose parent was deleted or cannot be found) and reattach them", "Re-initialize categories to defaults" ], "steps": [ @@ -1186,13 +1187,16 @@ "Drag and drop a category in the tree to reorder it or move it under a different parent", "Add keywords that match transaction descriptions for auto-categorization", "Set keyword priority to resolve conflicts when multiple categories match", - "Use the color picker to assign a custom color for charts" + "Use the color picker to assign a custom color for charts", + "Click Check hierarchy to list the orphan categories: for each one, choose a new parent (the root by default), then click Reattach" ], "tips": [ "Non-inputable categories are hidden from budget and transaction dropdowns but still visible in reports", "Higher priority keywords win when multiple categories match the same transaction", "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 with transactions of its own cannot become the parent of an existing category, since it would no longer take input: recategorize its transactions first, or choose another parent" ] }, "adjustments": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index b106798..9442c90 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1178,6 +1178,7 @@ "Glisser-déposer pour réordonner les catégories ou changer leur parent", "Basculer les catégories en saisissable ou non-saisissable", "Vue « Tous les mots-clés » pour voir l'ensemble des règles", + "« Vérifier la hiérarchie » pour retrouver les catégories orphelines (prises dans une boucle de parenté, ou dont le parent a été supprimé ou est introuvable) et les rattacher", "Réinitialiser les catégories aux valeurs par défaut" ], "steps": [ @@ -1186,13 +1187,16 @@ "Glissez-déposez une catégorie dans l'arbre pour la réordonner ou la déplacer sous un autre parent", "Ajoutez des mots-clés correspondant aux descriptions des transactions pour l'auto-catégorisation", "Définissez la priorité des mots-clés pour résoudre les conflits quand plusieurs catégories correspondent", - "Utilisez le sélecteur de couleur pour assigner une couleur personnalisée pour les graphiques" + "Utilisez le sélecteur de couleur pour assigner une couleur personnalisée pour les graphiques", + "Cliquez sur Vérifier la hiérarchie pour lister les catégories orphelines : pour chacune, choisissez un nouveau parent (la racine par défaut), puis cliquez sur Rattacher" ], "tips": [ "Les catégories non-saisissables sont masquées du budget et des menus déroulants mais restent visibles dans les rapports", "Les mots-clés de priorité supérieure l'emportent quand plusieurs catégories correspondent à la même transaction", "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 qui a des transactions à son nom ne peut pas devenir le parent d'une catégorie existante, puisqu'elle ne serait plus saisissable : reclassez d'abord ses transactions, ou choisissez un autre parent" ] }, "adjustments": {