[#6] Zoom categorie + AddKeywordDialog (scope limite) #74

Closed
opened 2026-04-13 15:06:49 +00:00 by maximus · 0 comments
Owner

Objectif

Implémenter le zoom catégorie (remplace l'ancien tableau croisé dynamique) + l'édition contextuelle des mots-clés via clic droit, limitée à CategoryTransactionsTable dans cette issue. La propagation du clic droit aux autres tables est traitée en Issue #75 (follow-up).

Cette issue porte l'essentiel des garanties de sécurité identifiées par le review expert.

Spec : spec-refonte-rapports.md
Dépend de #70 (parallélisable avec #71, #72, #73)

Tâches — Zoom catégorie

Service avec cycle guard obligatoire

  • Implémenter reportService.getCategoryZoom(categoryId, from, to, includeSubcategories) via CTE SQLite récursive bornée :
    WITH RECURSIVE cat_tree(id, depth) AS (
      SELECT id, 0 FROM categories WHERE id = ?1
      UNION ALL
      SELECT c.id, ct.depth + 1
      FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id
      WHERE ct.depth < 5
    )
    SELECT ... WHERE category_id IN (SELECT id FROM cat_tree);
    
  • Retourne { rollupTotal, byChild: { id, name, color, total }[], monthlyEvolution: { month, total }[], transactions: Transaction[] }
  • Test unitaire avec fixture cyclique (catégories A→B→A) qui doit retourner sans boucler grâce à la borne depth < 5

Hook

  • Compléter src/hooks/useCategoryZoom.ts : state { categoryId, includeSubcategories, data, loading, error }

Composants (plat dans src/components/reports/)

  • CategoryZoomHeader.tsx — combobox catégorie + toggle inclure sous-catégories (activé par défaut)
  • CategoryDonutChart.tsxtemplate de démarrage : src/components/dashboard/CategoryPieChart.tsx lignes 81–90 (déjà un <Pie innerRadius={35} outerRadius={75}> avec patterns)
  • CategoryEvolutionChart.tsx — AreaChart sur la période, patterns SVG
  • CategoryTransactionsTable.tsx — sortable, filtrable, expose onContextMenu sur chaque ligne

Page

  • Remplacer le placeholder de src/pages/ReportsCategoryPage.tsx
  • Compose : header + donut + evolution + table
  • ViewModeToggle partagé, storage key reports-viewmode-category

Tâches — Édition contextuelle des mots-clés

Export depuis categorizationService

  • Exporter depuis src/services/categorizationService.ts : normalizeDescription, buildKeywordRegex, compileKeywords (actuellement privés)

AddKeywordDialog — emplacement : src/components/categories/AddKeywordDialog.tsx

(Pas dans components/reports/ : c'est un composant du domaine édition mot-clé, utilisé aussi depuis /transactions plus tard.)

Garanties de sécurité (non négociables) :

  • Validation longueur : keyword obligatoire entre 2 et 64 caractères après .trim(), rejet whitespace-only, messages i18n reports.keyword.tooShort / tooLong. Prévient ReDoS (CWE-1333)
  • Preview via SQL paramétré : SELECT ... FROM transactions WHERE description LIKE ?1 — jamais d'interpolation de chaîne. Puis filtrage en mémoire avec le regex compilé par buildKeywordRegex. Prévient SQL injection (CWE-89)
  • Affichage limité à 50 matches ; au-delà, checkbox explicite « Appliquer aussi aux N-50 transactions non affichées » (off par défaut)
  • Apply dans une transaction SQL englobante via tauri-plugin-sql : BEGIN; INSERT keywords; UPDATE transactions; COMMIT; avec rollback + toast erreur en cas d'échec (CWE-662)
  • Application uniquement aux lignes cochées visibles, sauf si l'utilisateur a explicitement coché l'option des N-50 non affichées
  • Comportement « mot-clé déjà existant pour autre catégorie » : UPDATE keywords SET category_id=? WHERE keyword=? + re-run de la catégorisation uniquement sur les matches visibles cochés (jamais rétroactif sur l'historique complet). Afficher reports.keyword.alreadyExists avant validation
  • Rendu XSS-safe : descriptions rendues comme enfants React ({tx.description}) — jamais dangerouslySetInnerHTML, troncature CSS uniquement (CWE-79)

Branchement clic droit (scope limité)

  • Utiliser ContextMenu générique (créé en #69) avec un item « Ajouter comme mot-clé » qui ouvre AddKeywordDialog
  • Brancher uniquement sur CategoryTransactionsTable dans cette issue
  • Les autres tables (HighlightsTopMoversTable, ComparePeriodTable, etc.) seront branchées en Issue #75

i18n

  • Clés reports.category.* (selectCategory, includeSubcategories, directOnly, breakdown, evolution, transactions)
  • Clés reports.keyword.* (addFromTransaction, dialogTitle, willMatch, nMatches_one, nMatches_other, applyAndRecategorize, tooShort, tooLong, alreadyExists)
  • Format pluriel i18next v25 : _one / _other (pas _plural)
  • Parité FR/EN

Vérifications

  • /reports/category affiche le zoom complet avec rollup auto
  • Toggle « direct seulement » retire les sous-catégories
  • Clic droit sur transaction → menu → « Ajouter comme mot-clé » → dialog
  • Longueur < 2 ou > 64 → erreur bloquante, pas d'INSERT
  • Preview limite à 50 + checkbox N-50 visible
  • Appliquer → toast succès + transactions recatégorisées + keyword présent dans /categories
  • Test unitaire cycle guard passe (fixture A→B→A)
  • grep -n "dangerouslySetInnerHTML" src/components/categories/AddKeywordDialog.tsx retourne vide
  • npm run build + cargo check + npm test verts

Critères d'acceptation

  • /reports/category affiche donut + evolution + table pour une catégorie sélectionnée, avec rollup
  • getCategoryZoom termine avec fixture cyclique (test unitaire vert)
  • AddKeywordDialog respecte TOUTES les 7 garanties de sécurité listées ci-dessus
  • Clic droit fonctionne sur CategoryTransactionsTable
  • i18n plurals utilisent _one / _other
## Objectif Implémenter le **zoom catégorie** (remplace l'ancien tableau croisé dynamique) + l'**édition contextuelle des mots-clés** via clic droit, **limitée à `CategoryTransactionsTable` dans cette issue**. La propagation du clic droit aux autres tables est traitée en Issue #75 (follow-up). Cette issue porte l'essentiel des garanties de sécurité identifiées par le review expert. Spec : `spec-refonte-rapports.md` Dépend de #70 (parallélisable avec #71, #72, #73) ## Tâches — Zoom catégorie ### Service avec cycle guard obligatoire - [ ] Implémenter `reportService.getCategoryZoom(categoryId, from, to, includeSubcategories)` via **CTE SQLite récursive bornée** : ```sql WITH RECURSIVE cat_tree(id, depth) AS ( SELECT id, 0 FROM categories WHERE id = ?1 UNION ALL SELECT c.id, ct.depth + 1 FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id WHERE ct.depth < 5 ) SELECT ... WHERE category_id IN (SELECT id FROM cat_tree); ``` - [ ] Retourne `{ rollupTotal, byChild: { id, name, color, total }[], monthlyEvolution: { month, total }[], transactions: Transaction[] }` - [ ] **Test unitaire** avec fixture cyclique (catégories A→B→A) qui doit retourner sans boucler grâce à la borne `depth < 5` ### Hook - [ ] Compléter `src/hooks/useCategoryZoom.ts` : state `{ categoryId, includeSubcategories, data, loading, error }` ### Composants (plat dans `src/components/reports/`) - [ ] `CategoryZoomHeader.tsx` — combobox catégorie + toggle `inclure sous-catégories` (activé par défaut) - [ ] `CategoryDonutChart.tsx` — **template de démarrage : `src/components/dashboard/CategoryPieChart.tsx` lignes 81–90** (déjà un `<Pie innerRadius={35} outerRadius={75}>` avec patterns) - [ ] `CategoryEvolutionChart.tsx` — AreaChart sur la période, patterns SVG - [ ] `CategoryTransactionsTable.tsx` — sortable, filtrable, expose `onContextMenu` sur chaque ligne ### Page - [ ] Remplacer le placeholder de `src/pages/ReportsCategoryPage.tsx` - [ ] Compose : header + donut + evolution + table - [ ] `ViewModeToggle` partagé, storage key `reports-viewmode-category` ## Tâches — Édition contextuelle des mots-clés ### Export depuis `categorizationService` - [ ] **Exporter** depuis `src/services/categorizationService.ts` : `normalizeDescription`, `buildKeywordRegex`, `compileKeywords` (actuellement privés) ### `AddKeywordDialog` — emplacement : `src/components/categories/AddKeywordDialog.tsx` (Pas dans `components/reports/` : c'est un composant du domaine édition mot-clé, utilisé aussi depuis `/transactions` plus tard.) **Garanties de sécurité (non négociables) :** - [ ] **Validation longueur** : keyword obligatoire entre **2 et 64 caractères** après `.trim()`, rejet whitespace-only, messages i18n `reports.keyword.tooShort` / `tooLong`. Prévient ReDoS (CWE-1333) - [ ] **Preview via SQL paramétré** : `SELECT ... FROM transactions WHERE description LIKE ?1` — jamais d'interpolation de chaîne. Puis filtrage en mémoire avec le regex compilé par `buildKeywordRegex`. Prévient SQL injection (CWE-89) - [ ] **Affichage limité à 50 matches** ; au-delà, checkbox explicite « Appliquer aussi aux N-50 transactions non affichées » (off par défaut) - [ ] **Apply dans une transaction SQL englobante** via `tauri-plugin-sql` : `BEGIN; INSERT keywords; UPDATE transactions; COMMIT;` avec rollback + toast erreur en cas d'échec (CWE-662) - [ ] Application **uniquement aux lignes cochées visibles**, sauf si l'utilisateur a explicitement coché l'option des N-50 non affichées - [ ] **Comportement « mot-clé déjà existant pour autre catégorie »** : `UPDATE keywords SET category_id=? WHERE keyword=?` + re-run de la catégorisation **uniquement** sur les matches visibles cochés (jamais rétroactif sur l'historique complet). Afficher `reports.keyword.alreadyExists` avant validation - [ ] **Rendu XSS-safe** : descriptions rendues comme enfants React (`{tx.description}`) — jamais `dangerouslySetInnerHTML`, troncature CSS uniquement (CWE-79) ### Branchement clic droit (scope limité) - [ ] Utiliser `ContextMenu` générique (créé en #69) avec un item « Ajouter comme mot-clé » qui ouvre `AddKeywordDialog` - [ ] **Brancher uniquement sur `CategoryTransactionsTable`** dans cette issue - [ ] Les autres tables (`HighlightsTopMoversTable`, `ComparePeriodTable`, etc.) seront branchées en Issue #75 ### i18n - [ ] Clés `reports.category.*` (selectCategory, includeSubcategories, directOnly, breakdown, evolution, transactions) - [ ] Clés `reports.keyword.*` (addFromTransaction, dialogTitle, willMatch, `nMatches_one`, `nMatches_other`, applyAndRecategorize, tooShort, tooLong, alreadyExists) - [ ] **Format pluriel i18next v25** : `_one` / `_other` (pas `_plural`) - [ ] Parité FR/EN ### Vérifications - [ ] `/reports/category` affiche le zoom complet avec rollup auto - [ ] Toggle « direct seulement » retire les sous-catégories - [ ] Clic droit sur transaction → menu → « Ajouter comme mot-clé » → dialog - [ ] Longueur < 2 ou > 64 → erreur bloquante, pas d'INSERT - [ ] Preview limite à 50 + checkbox N-50 visible - [ ] Appliquer → toast succès + transactions recatégorisées + keyword présent dans `/categories` - [ ] Test unitaire cycle guard passe (fixture A→B→A) - [ ] `grep -n "dangerouslySetInnerHTML" src/components/categories/AddKeywordDialog.tsx` retourne vide - [ ] `npm run build` + `cargo check` + `npm test` verts ## Critères d'acceptation - `/reports/category` affiche donut + evolution + table pour une catégorie sélectionnée, avec rollup - `getCategoryZoom` termine avec fixture cyclique (test unitaire vert) - `AddKeywordDialog` respecte TOUTES les 7 garanties de sécurité listées ci-dessus - Clic droit fonctionne sur `CategoryTransactionsTable` - i18n plurals utilisent `_one` / `_other`
maximus added this to the spec-refonte-rapports milestone 2026-04-13 15:06:49 +00:00
maximus added the
status:ready
type:feature
type:security
source:human
labels 2026-04-13 15:06:49 +00:00
maximus added
status:approved
and removed
status:ready
labels 2026-04-14 19:10:15 +00:00
Sign in to join this conversation.
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#74
No description provided.