feat(onboarding): Transactions page step card and guided entry #405
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#405
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-399-onboarding-transactions-card"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resolves #399
Generated autonomously by /autopilot run of 2026-10-08
Ce qui change pour l'utilisateur
Technique
useTransactions(initialFilters?):useReducer(transactionsReducer, initialFilters, initTransactionsState), filtres de départ posés sur les défauts et lus au montage seulement. Le reducer reste exporté ; nouvel étatguidedFallback.GUIDED_ENTRY_FILTERS = { dateFrom: null, uncategorizedOnly: true }, que la page passe quandreadOnboardingNavState(location.state).guidedest vrai (seule lecture de l'état de navigation, CWE-476).SET_PAGE_RESULTavectotalCount === 0etguidedFallback, le reducer retireuncategorizedOnly(dateFromnul gardé), remet la page à 1, laisseisLoadingvrai et crée un nouvel objetfilters: l'effet existant sur les filtres relit, après son attente de 300 ms. La première page, quel que soit son compte, et toutSET_FILTERdésarment le repli : une seule fois, pas de boucle, jamais un filtre de l'utilisateur retiré dans son dos. Prédicat inchangé (getTransactionPage), pas de compteuruncategorized.transactionsCardModel(filters, checklist)(onboardingPath.ts) : texteuncategorized(aveclabels: { filter: "transactions.filters.uncategorized" }) ouall, ciblefirstRowCategory;{ text: null, done: true, target: null }une fois l'étape cochée.CARD_PAGESreçoit/transactions.TransactionTablereçoitonboardingTarget?: boolean: un enveloppeurdiv.rounded-lgautour duCategoryComboboxde chaque ligne, et seul celui de la première portedata-onboarding-target="transaction-category"etdata-onboarding-highlight.CategoryComboboxne change pas.TransactionsPagerendOnboardingStepCarden premier enfant, avant son en-tête, et ne passe la cible que si la carte est affichée (showsStepCard), comme la page Import.lowercaseenregistré danssrc/i18n/config.ts; les phrases FR et EN écrivent{{filter, lowercase}}.docs/architecture.md(ligneuseTransactions, section Internationalisation). Aucun changement Rust, aucune migration.Preuves
npm run buildvert (tsc + vite).npx vitest run: 1814 → 1844 tests verts (101 → 103 fichiers).useTransactions.test.ts(+8) : filtres de départ, visite ordinaire, repli unique, pas de boucle, filtre de l'utilisateur jamais retiré.onboardingPath.test.ts(+8) :CARD_PAGES,transactionsCardModel(deux textes, « Fait. », étape passée), et les deux phrases résolues par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue (filtre en minuscules).TransactionTable.render.test.tsx(nouveau, +4) : exactement un élément mis en évidence, l'enveloppeur de la catégorie de la première ligne, sans les boutons mot-clé et fractionnement.TransactionsPage.render.test.tsx(nouveau, +10) : page rendue avec le vraiuseTransactions; entrée guidée sans borne de date, visite ordinaire, état de navigation malformé, carte avant le titre, une seule cible, aucune cible sans carte, « Fait. » puis l'étape Budget, fin en Gratuite.AppShell.render.test.tsx: /transactions sort de la table « bandeau tant qu'aucune carte n'est livrée » et gagne son test de carte, comme /import dans #404.git checkout; 5 fichiers de test concernés, 108 tests verts avant chaque mutation :guided(aucun filtre de départ)totalCount < 0)index >= 0)!uncategorizedOnly)CARD_PAGESsans/transactions8/8 rouges, arbre propre après chaque restauration.
Décisions
12 décisions consignées dans le journal du run ; les principales :
transactionsCardModelprend aussi la liste (checklist), commeimportCardModel: sans elle, pas dedone, donc ni « Fait. » ni « Étape suivante » (critère d'acceptation). La signature de l'issue ne portait que{ uncategorizedOnly }.{{filter}}par un formateur i18nextlowercase(config.ts, hors de la liste de fichiers de l'issue) : la phrase de chaque langue décide de la casse, et le libellé reste interpolé depuis sa clé. Écartés : la minuscule dansStepCard(composant de #404) ; une clé recopiée en minuscules.SET_FILTER; relecture par l'effet existant sur les filtres (300 ms), « Chargement… » entre-temps. Écarté : un jeton de relecture immédiate.target, null après « Fait. »), commeImportCardModel.Pour relire
src/hooks/useTransactions.ts:initTransactionsState,SET_PAGE_RESULT,SET_FILTER. Aucune action ne doit réarmer le repli.src/utils/onboardingPath.ts:transactionsCardModeletCARD_PAGES.src/components/transactions/TransactionTable.tsx: l'enveloppeur. Mise en page attendue inchangée : lediv.relativedu combobox devient l'enfant d'undivsans classe de taille.src/i18n/config.ts, et les deux phrases dansfr.jsoneten.json.fetchDataenvoieSET_LOADINGpuisSET_ERROR(null), qui remetisLoadingà faux au début de chaque lecture. Après le repli, « Aucune transaction trouvée. » peut donc s'afficher le temps de la seconde lecture (quelques millisecondes), après « Chargement… ».🤖 Generated with Claude Code
The link of the "categorize by hand" step carries `guided`, which the Transactions page reads through `readOnboardingNavState` and turns into start filters: `useTransactions(initialFilters?)` puts them over the defaults through `initTransactionsState`, at mount only, so the card consuming the navigation state changes nothing. The guided entry lists the uncategorized transactions whatever their date (the default filter starts on January 1st, and in January the sample statements fall in the previous year). When its first page comes back empty, the reducer drops "uncategorized only" once: a new filters object, so the existing filter effect reads again, and the loading placeholder meanwhile. The first page and any filter the user sets disarm the fallback, so it never loops and never drops a filter behind the user's back. `transactionsCardModel(filters, checklist)` says which sentence matches the list, names the filter by its own label, points at the first row's category, and says "Done." once the step is checked, which the card needs to offer the Budget step or the end of the path. The label loses its capital through an i18next `lowercase` formatter used by the sentence itself, `{{filter, lowercase}}`. TransactionTable wraps every row's category in the same element and puts the ring on the first one only, so the ring coming or going never remounts the combobox; CategoryCombobox is unchanged. CARD_PAGES gets /transactions, and the page renders the card first and highlights its target only while the card shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Auto-revue de l'autopilot (#399)
Verdict : APPROVE. C'est l'auto-revue du worker. L'issue passe en
status:review, pas enstatus:approved: la revue indépendante reste à faire.Relu sur le diff de cette PR seule (15 fichiers, tête
9a695a2).Critères d'acceptation
Reducer de
useTransactions(tests) : les filtres de départ sont appliqués parinitTransactionsState. Le filtre guidé se replie surtotalCount === 0une seule fois : la première page et toutSET_FILTERle désarment. Une visite ordinaire garde les filtres par défaut. 8 tests, et les mutations M2, M3 et M4 rougissent.TransactionTable: aveconboardingTarget, un seul élément est mis en évidence, l'enveloppeur de la catégorie de la première ligne. Les boutons mot-clé et fractionnement restent hors de l'anneau. Le test de rendu le vérifie, et M5 rougit.transactionsCardModel: les deux textes. Un test les résout aussi par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue, filtre en minuscules. M6 rougit.Profil neuf, arrivée par « Étape suivante » : couvert au niveau de la page par
TransactionsPage.render.test.tsx, avec le vraiuseTransactions:Le cochage en direct repose sur la notification de
updateTransactionCategory(ADR 0022, #402) : il reste à vérifier à la main.FR, EN et CI : FR et EN, CHANGELOG FR et EN (Ajouté),
npm run buildvert,npx vitest runà 1844/1844. La CIcheck-frontend(run 503) est verte : build, 103 fichiers et 1844 tests, lus dans le journal du job.Review caveats
readOnboardingNavState. Un test passe quatre états malformés, que la page lit comme une visite ordinaire.filters, donc rien ne relit ; M3 (premier résultat qui ne désarme pas) rougit trois tests.Conventions
utils/et l'état dans le reducer du hook ; les composants sont pilotés par props.CategoryComboboxest intact.Constats (aucun bloquant)
transactionsCardModel(filters, checklist)au lieu de({ uncategorizedOnly }). Sans la liste, pas dedone, donc ni « Fait. » ni « Étape suivante ». M8 (le « Fait. » lu sur l'étape Budget) rougit trois tests.src/i18n/config.tsreçoit le formateurlowercase. C'est la phrase de chaque langue qui décide de la casse ({{filter, lowercase}}).fetchDataenvoieSET_ERROR(null)au début de chaque lecture, ce qui remetisLoadingà faux. Après le repli, « Aucune transaction trouvée. » peut donc s'afficher quelques millisecondes, après les 300 ms de « Chargement… ».div.rounded-lgsous WebKitGTK ;Passe de mutations (arbre commité)
8 mutations, 8 rouges : M1 = 2, M2 = 1, M3 = 3, M4 = 2, M5 = 3, M6 = 5, M7 = 2, M8 = 3. Le détail est dans la description de la PR.
Revue adversariale : PR #405 (#399)
Verdict : APPROVE
Résumé. L'entrée guidée, le repli unique et la carte de la page Transactions font ce que l'issue demande, sans bloquant. La suite et dix mutations du relecteur ont tourné sur une copie hors dépôt de la tête
9a695a2. Quatre mutations restent vertes : trois sur le reducer, qui casseraient le repli dans l'app sans qu'aucun test le voie, et une sur la page. Les deux tests qui les ferment tiennent en quelques lignes. Ce sont des épingles sur un code correct, donc des suggestions.Vérifications
git archivede9a695a2(npm ci) :npm run buildvert,npx vitest runà 103 fichiers et 1844 tests verts. Le journal du run 503 (check-frontend, checkout de9a695a2) donne les mêmes chiffres.notifyDataChanged()deupdateTransactionCategory(ADR 0022, #402) et reste dans la liste QA de #401.readOnboardingNavState. Le repli ne boucle pas : la première page et toutSET_FILTERle désarment, et seulinitTransactionsStatel'arme (aucune action du reducer ne le réarme).pull_refclosesvenant de #405, etResolves #399figure dans le message de9a695a2. Un merge ff local la ferme donc aussi.Points relus sans constat
keysur la route/transactions). Lenavigate(pathname, { replace: true, state: null })de la carte ne touche donc pas aux filtres de départ, comme l'annonce le commentaire de la page.filters. Le nettoyage de l'effet[state.filters]annule alors le minuteur posé au montage : aucune seconde lecture avec les filtres guidés ne part après le repli. Une réponse périmée reste écartée parfetchIdRef. Si la première lecture dépasse 300 ms, la réponse du minuteur devient la première page, et le repli s'applique à elle.CategoryComboboxest undiv.relativesans taille, et soninputest enw-full. L'enveloppeur sans classe de taille ne change donc pas la largeur de la colonne. L'anneau (3 px, décalé de 2 px) tient dans lepx-3 py-2de la cellule, sous l'overflow-x-autodu tableau.{{filter, lowercase}}n'apparaît que dans ces deux phrases, et le formateur est présent aprèsinit: le test résout les deux phrases par la vraie instance, en FR et en EN.autoCategorizeTransactionsne lit quecategory_id IS NULL AND is_manually_categorized = 0.Mutations du relecteur
Une à la fois, sur la copie, restaurées par
git checkout, arbre propre après chacune. Les cinq fichiers de test de la PR, 108 tests verts avant chaque mutation.SET_ERRORdésarme le repliSET_TOTAL_IN_DBdésarme le repliSET_LOADINGdésarme le repliguided) au lieu destate.filterspageà 1isLoadingcardModel.targetau lieu detarget)lowercasesans mise en minusculesSuggestions (non bloquantes)
useTransactions.test.ts). Les tests envoientSET_PAGE_RESULTdirectement aprèsinitTransactionsState. OrfetchDataenvoieSET_LOADING(true)puisSET_ERROR(null)avant chaqueSET_PAGE_RESULT, et l'effet du montage envoieSET_CATEGORIES,SET_SOURCESetSET_TOTAL_IN_DB, avant ou après la première page selon l'ordre d'arrivée des requêtes. Sous R1 ou R3, le repli ne se déclencherait jamais dans l'app ; sous R2, selon cet ordre. Les 108 tests restent verts dans les trois cas. Un test qui réduit[SET_LOADING(true), SET_ERROR(null), SET_CATEGORIES, SET_SOURCES, SET_TOTAL_IN_DB, SET_PAGE_RESULT(0)]depuisinitTransactionsState(GUIDED_ENTRY_FILTERS)et attenduncategorizedOnly: false,dateFrom: nulletisLoading: truepasse sur9a695a2et rougit sous R1, R2 et R3 (mesuré sur la copie).TransactionsPage.render.test.tsx). Le harnais remplace déjàrows; s'il remplace aussifilters, deux cas suffisent : une entrée guidée après le repli (uncategorizedOnly: false) donne la phraseall, une visite ordinaire avec « Non catégorisées » choisi à la main donne la phraseuncategorized. Les deux passent sur9a695a2et rougissent sous R4 (mesuré). Sans eux, une page qui calculerait la phrase depuisguidedafficherait « Seules les transactions non catégorisées sont affichées » au-dessus de toutes les transactions après le repli, et la phrase basculerait dès que la carte consomme l'état de navigation.isLoading: truedu repli y affiche aussi la barre de filtres et « Chargement… » environ 300 ms à la place de l'état vide.Les suggestions 1 et 2 peuvent s'appliquer en tête de pile (#407), comme pour les suites de licence, sans toucher aux têtes déjà revues. Merge recommandé.
Actions faites
/tmp/pr405-review(git archivede9a695a2, puisnpm ci) : build, suite complète, 10 mutations, et 2 fichiers de test jetables (séquence du reducer, phrase de la carte) pour mesurer les suggestions 1 et 2./tmp/pr405-*supprimés en fin de revue. Aucun fichier du dépôt modifié.