feat(onboarding): empty states on the Transactions and Budget pages (#353) #379
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#379
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-353-transactions-budget-empty"
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?
Fixes #353
Stacked on PR #378 (
issue-352-dashboard-empty-state), which brings the sharedEmptyStatecomponent andgetTransactionCount(). Merge #378 first; this PR's base must be rebased tomainafterwards.What
src/hooks/useTransactions.ts:transactionsReducer+initialTransactionsStateexported; newtotalInDb: number | nullloaded once at mount alongside categories and sources (Promise.all+getTransactionCount()); pure selectorshowsEmptyState(state)=totalInDb === 0 && !isLoading.src/hooks/useBudget.ts:budgetReducer+initialBudgetState()exported (kept lazy,yearis read at mount);totalInDbloaded in the existing once-on-mount effect with the import sources;showsEmptyState(state)=totalInDb === 0.TransactionsPage: when the selector is true, rendersEmptyState(icon, title, description, primary action to/import) instead of the filter bar, summary bar, table and pagination. Otherwise unchanged; the error banner stays above both branches.BudgetPage: rendersEmptyStateinstead ofFilterPanel+BudgetTable, keeping the title,PageHelpandTemplateActions.RequireFeature feature="budget"inApp.tsxuntouched.transactions.empty.{title,description,cta},budget.empty.{title,description,cta}.Why
The default Transactions filter is "since January 1st" (
useTransactions.ts), sorows.length === 0cannot distinguish an empty profile from a year without transactions; a profile-wide count is the only reliable signal.null(count not loaded yet) keeps the regular layout so nothing flashes.How tested
src/hooks/useTransactions.test.ts(6 tests):totalInDbstartsnull;SET_TOTAL_IN_DBkeeps the samefilters/sort/pagereferences; 0 shows the empty state then N > 0 renders the table; loading at 0 keeps the regular layout; an empty page on a populated profile is not the empty state; other actions preserve the count.src/hooks/useBudget.test.tsextended (4 tests): same invariants onyear/rows/templates/accounts.TransactionTable("Aucune transaction trouvée") andBudgetTable("Aucune catégorie trouvée") are untouched and still rendered on a populated profile whose filter returns nothing.npm test: 75 files, 1411 tests green.npm run build(tsc + vite) green. No hard-coded UI strings in the touched components.Generated autonomously by /autopilot run of 2026-10-03
🤖 Generated with Claude Code
Self-review (autopilot 2026-10-03) — Verdict: APPROVE
Summary. The diff is confined to the two hooks, the two pages, the two locale files and two test files. Both reducers are exported with their initial state,
totalInDbis a new orthogonal field (null→ count), and a pureshowsEmptyState(state)selector per hook is what each page branches on, so the 0 → N transition is covered by the reducer tests rather than by a renderer the repo does not have.CHANGELOG*,App.tsx(RequireFeature),TransactionTable,BudgetTableand the DB are untouched.Security. No new input surface: one
SELECT COUNT(*) FROM transactionson the local SQLite (existinggetTransactionCount()from #378), and a<Link to="/import">to an internal route throughEmptyState. Nothing leaves the machine.transactionsReducer+initialTransactionsStateexported (private reducer at:80)useTransactions.ts—useReducer(transactionsReducer, initialTransactionsState)budgetReducer+ initial state exported (:47)initialBudgetState()kept lazy (yearread at mount),useReducer(budgetReducer, undefined, initialBudgetState)totalInDb: number | null, loaded once at mount with categories/sources viagetTransactionCount()Promise.allwith the count; never re-fetchedtotalInDbdoes not touchfilters/sort/pageuseTransactions.test.tsasserts same references, plus full-state equality modulototalInDb;useBudget.test.tsdoes the same foryear/rows/templates/accountsshowsEmptyStatetrue at 0, false at 42 / 12, in both filesEmptyStatewhentotalInDb === 0 && !isLoading, replacing filter bar + table + pagination; otherwise unchangedTransactionsPage; summary bar is also hidden in the empty branch (it only shows 0 $ otherwise); the error banner stays above both branchesEmptyStatereplacingFilterPanel+BudgetTablewhentotalInDb === 0, keeping title,PageHelp,TemplateActionsrows.length === 0is not the signal (default filter since Jan 1st)TransactionTable/BudgetTableunchanged and rendered whenever the count isnullor > 0transactions.empty.{title,description,cta},budget.empty.{title,description,cta}in both files; build + full suite parse themt()npm test,npm run buildgreenConcerns (non-blocking).
nullkeeps the layout) and avoids flashing the card on populated profiles.BudgetPagekeeps itsopacity-50 pointer-events-nonewrapper whilerefreshDataruns, so the empty card's button is briefly non-clickable during the initial load, exactly like the grid was. Not a regression.showsEmptyState; each page imports it from its own hook, so there is no ambiguity, but a future shared import would need aliasing.Generated autonomously by /autopilot run of 2026-10-03
Revue adversariale — Verdict : APPROVE
Résumé. La PR fait ce qu'elle annonce : un compteur
totalInDbchargé une fois au montage (SELECT COUNT(*) FROM transactions, hérité de #378), un sélecteur purshowsEmptyStatepar hook, et un seul ternaire par page qui remplace filtres + table/grille parEmptyState. Diff lu en entier contre sa baseissue-352-dashboard-empty-state(1 commit, 8 fichiers), pas contremain. Aucune migration, aucune chaîne en dur, pas deskip/only, clés i18n FR/EN présentes sans doublon (vérifié par un parseur JSON à détection de clés dupliquées),Resolves #353dans le message de commit et dans le corps → la timeline de #353 porte bienclosesdepuis cette PR, et un merge localff-onlyfermera aussi l'issue.Exécuté par le relecteur (copie hors dépôt via
git archive 8eac47f, supprimée en fin de revue) :tsc --noEmit: exit 0.vitest run: 75 fichiers / 1411 tests verts — même chiffre que la PR.PR Check — Frontendsuccess (1m51s) — la PR stackée a bien eu sa CI (aucun filtrebranches:).cmp) :showsEmptyState(transactions) sans le garde!isLoadingSET_TOTAL_IN_DB(transactions) remetfiltersetpageà zéroshowsEmptyState(transactions) confondnullet0showsEmptyState(budget) confondnullet0SET_TOTAL_IN_DB(budget) viderowsLes tests tiennent sur le point qui compte : le sélecteur distingue « pas encore chargé » de « zéro », et le compteur ne touche pas l'état de pagination (références identiques, pas seulement égalité profonde).
Autres vérifications.
BrowserRouter key={refreshKey}(App.tsx:109) remonte les pages au changement de profil, donc le compteur chargé « une fois au montage » est bien rechargé par profil — pas de compteur périmé entre deux profils.EmptyStateaccepte{label, to}et rend un<Link>, pas deuseNavigate: conforme à son contrat. LeRequireFeature feature="budget"d'App.tsxest intact.Suggestions (non bloquantes)
isEmptyProfile ?dans l'une ou l'autre page laisserait les 1411 tests verts — seultscles voit. C'est la convention du dépôt (pas de renderer), donc pas un blocage ; c'est le trou à connaître avant de toucher à ces pages.Promise.all(useTransactions.ts:171-180,useBudget.ts:435-441) : un échec degetTransactionCount()fait désormais échouer aussi le chargement des catégories/sources (Transactions) ou des comptes (Budget), ce qui n'était pas le cas avant. Impact réel faible (COUNT(*)qui échoue = base cassée), maisPromise.allSettledou untryséparé garderait l'ancien comportement.!isLoadingsur Transactions allonge le flash au lieu de l'éviter. Son commentaire parle d'« un profil dont le compte est déjà connu », mais le compte n'est jamais connu avant le montage (chargé dans le même effet). Sur un profil vide, la mise en page régulière (barre de filtres + barre de synthèse + « Chargement… ») s'affiche donc jusqu'à la fin de la première page, puis bascule sur la carte. Le « nothing flashes » du corps de PR dit l'inverse de ce qui se passe ; sans le garde, la bascule aurait lieu dès l'arrivée du compte. Sans conséquence sur SQLite local (quelques ms), et incohérent avec le sélecteur Budget qui n'a pas ce garde — à aligner un jour, dans un sens ou l'autre.autoCategorizeNone. Cosmétique.BudgetPagegarde son wrapperopacity-50 pointer-events-nonependantrefreshData, donc la carte est brièvement grisée et non cliquable au premier chargement — déjà noté par la self-review, non-régression.Actions faites. Copie hors dépôt
scratchpad/pr379supprimée, aucun fichier/tmp/pr379-*laissé. Label de #353 : déjàstatus:approved(posé par la self-review autopilot), inchangé.Revue /pr-review du 2026-10-03 — mutations et suite rejouées par le relecteur, pas relayées.
maximus referenced this pull request2026-10-04 12:52:55 +00:00