feat(onboarding): dashboard empty state, getting-started checklist and re-show button (#352) #378
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#378
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-352-dashboard-empty-state"
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 #352
Stacked on PR #377 (
issue-351-onboarding-core, issue #351, not merged): this PR's base is that branch and its diff only shows the #352 work.What
A profile without any transaction opens a dashboard that says what to do: a welcome card (
DashboardEmptyState) with the expanded getting-started checklist and one primary action towards/import, instead of widgets showing 0 $. With transactions, the compact checklist sits between the title and the filter panel until it is complete or dismissed. Paramètres → Utilisateurs → Guide gets a "Afficher la liste de démarrage" button, visible only when the dismissal preference is set.Why
Onboarding phase 1 (
planned-2026-10-03-onboarding-phase1): the analysis indocs/analyse-onboarding-2026-09.mdfound that a new profile lands on a page of empty charts with no next step.How
src/components/shared/EmptyState.tsx— pure; dashed card fromSourceList; atoaction renders a react-router<Link>(neveruseNavigate), anonClickaction renders a<button>withdisabled.src/components/dashboard/OnboardingChecklist.tsx— pure (onlyuseTranslation); propsitems/done/total/mode/budgetReady/onHide; undone rows link to theirto, done rows have no link; the locked Budget row shows a lock, the "Base et plus" label and links to/settings/users; whilebudgetReadyis false the budget row is neutral and the counter is hidden; compact mode = one labelled icon per row on a line, counter, hide button, no sentences.src/components/dashboard/DashboardEmptyState.tsx— title, sentence, expanded checklist (omitted whenhidden), primary<Link to="/import">.src/pages/DashboardPage.tsx— branches on the hookstatus:loading→ title +PageHelp+ banner only;error→ v0.15.0 page without the list +logWarn;readyandcounters.transactions === 0→ empty state rendered outside theopacity-50 pointer-events-nonewrapper;readyand > 0 → current page with the compact checklist unlesshiddenorcomplete.src/hooks/useOnboardingChecklist.ts— the return gainscountersanderror(additive; the issue has the page readcounters.transactionsand log the error).src/pages/settings/UsersSettingsPage.tsx— readsonboarding_checklist_hiddenat mount; button →showChecklist()then an inline confirmation replaces it;logErroron failure.fr.json/en.json— 16 keys:dashboard.empty.*,onboarding.checklist.*(title, counter, hide, lockedBudget, items.*),settings.users.onboarding.*.No DB migration. CHANGELOG untouched (centralised in #357).
Decisions (autopilot)
Tests
EmptyState.test.tsx(7) andOnboardingChecklist.test.tsx(9) by direct call, FilterPanel pattern:tovariant renders aLink,onClickvariant a button withdisabled; lock in Free with counter 1 / 5; neutral row and absent counter whenbudgetReady=false; compact mode without sentences; one link per undone row in both modes.npm test: 74 files, 1401 tests green.npm run buildgreen.grepof the new components: no hard-coded string; FR/EN key sets identical.Generated autonomously by /autopilot run of 2026-10-03
🤖 Generated with Claude Code
Self-review (autopilot, 2026-10-03) — Verdict: APPROVE
Reviewed
GET /pulls/378.diff(vsissue-351-onboarding-core, 10 files, +~900 lines), on the final head3f35313.Summary. The page branches on the hook
status, never on a bare counter; the empty state is returned before theopacity-50 pointer-events-nonewrapper, so its<Link to="/import">is clickable;loadingrenders only the banner, title andPageHelp;errorrenders the v0.15.0 page without the list and emits onelogWarn. The two new components are pure and tested by direct call. One quality finding (settings state names that read backwards:"visible"meant "no button") was fixed in3f35313before this verdict./importsans voileDashboardPage.tsxearly returnchecklistReady && counters.transactions === 0→<div>{header}<DashboardEmptyState/></div>, no veil class; counter asserteddone=1,total=6anddone=1,total=5inOnboardingChecklist.test.tsxloading: ni widgets ni état vide ;error: page actuelle sans listeif (status === "loading") return <div>{header}</div>;checklistReadyrequiresstatus === "ready", so no list onerror;logWarnin auseEffectkeyed onstatus/errorshowCompactChecklist = checklistReady && !hidden && !checklist.complete; the rest of the tree is byte-identical to the base (header moved into a fragment, same DOM)OnboardingChecklist.test.tsx: cadenas en Free, case neutre + compteur absent sibudgetReady=false, mode compact, liens par caselockedBudget+ link/settings/users; neutralCircle, noLock/CheckCircle2, no link, no counter; compact withoutitems.*text, 3 done / 3 todo icons,aria-labelper icon; links===undone rows'toin both modesEmptyState.test.tsx: variantesto(Link) etonClick(bouton,disabled)type === Linkwithprops.to,type === "button"withtype="button",disabledfalse/true, handler called, both actions ordered, children between description and actionsUsersSettingsPage: preference read at mount, row rendered only fordismissed/restored; click →showChecklist()then the confirmation line;logErroron failure, button keptnpm test,npm run buildverts ;grepsans chaîne en durtsc --noEmitgreen after the rename; grep over the five touched components hits only one code comment; FR/EN flattened key sets identical (16 new keys)Security. No user input is rendered; every
tocomes fromcomputeChecklistconstants; no new network call, no DB write beyond the existingshowChecklist()(preference delete). No migration.i18n. Same 16 keys in
fr.jsonanden.json, both files valid JSON and byte-stable under the project'sindent=2dump. Counter uses{{done}} / {{total}}interpolation.Accessibility. Actions are
<button type="button">or<Link>; compact icons carryaria-label/title, decorative iconsaria-hidden; both checklist modes arerole="region"with a label; the confirmation line isrole="status".Concerns (non-blocking).
logErrors a failed load; the page adds thelogWarnthe issue asks for, so one failure writes two Journal lines. Kept as specified.useDashboardstill fires its queries on an empty database (accepted in the issue's planning decisions).Generated autonomously by /autopilot run of 2026-10-03
Revue indépendante (
/pr-review, 2026-10-03) — Verdict : APPROVEPremière passe indépendante : le commentaire précédent est l'auto-revue de l'autopilot. Diff
issue-352-dashboard-empty-state(3f35313) contreissue-351-onboarding-core(76fd8d3), 10 fichiers.Ce qui a été exécuté
git archivede la tête,node_moduleslié) :EmptyState.test.tsx+OnboardingChecklist.test.tsx+useOnboardingChecklist.test.ts→ 26/26 verts ;tscnu (celui du scriptbuild) → exit 0. CIPR Check — Frontendverte sur la tête (1m42s), donc la PR stackée a bien eu sa CI.diff -q) :Link(expanded)budgetReady=falselockedjamais produittorendue en<button>disablednon propagéfr-only=en-only= ∅ ; aucune clé dupliquée au même niveau (object_pairs_hook), ce queJSON.parsene détecte pas ; aucune chaîne en dur dans les trois composants ; aucunskip/only.closes, venant de #378. Aucune migration, aucun secret, pas de CHANGELOG (centralisé en #357, conforme aux règles de la vague).Critères d'acceptation revérifiés dans le code
status, jamais sur un compteur nu ; tous les hooks (useDashboard,useOnboardingChecklist,useEffect, 2useState, 3useCallback,useMemo) sont appelés avant le premierreturnanticipé — pas de violation des règles des hooks.opacity-50 pointer-events-none: le<Link to="/import">reste cliquable.loading→ header seul ;error→checklistReadyfaux, page v0.15.0 sans liste, unlogWarnpar transition (useEffectsur[status, error]).completene peut pas être vrai à 0 transaction (manualCategorizedcompte des transactions), donc l'état vide n'a pas besoin de le lire.dismissed | restoredseulement, lecture au montage avec gardecancelled,logErrorsur échec ; le tableau de bord remonte à chaque navigation (etBrowserRouter key={refreshKey}au changement de profil), donc la préférence supprimée est relue sans état partagé.Suggestions (non bloquantes)
DashboardPage.tsx:803—completependant le chargement de la licence.useEntitlement().allowedest fail-closed tant quereadyest faux, donccomputeChecklistcompte Budget comme verrouillée ettotalvaut 5. Au boot à froid, un profil Base+ avec 5 lignes faites et aucun budget voit la liste compacte masquée (5/5) puis réapparaître (5/6) quand la licence arrive.const showCompactChecklist = checklistReady && !onboarding.hidden && !(onboarding.budgetReady && checklist.complete);ferme la fenêtre. Elle est étroite :LicenseProviderest dansmain.tsx, au-dessus du remount du routeur, donc absente au changement de profil.aria-hiddenetaria-label/titlene portent que le libellé : un lecteur d'écran n'entend ni « fait » ni « verrouillé », en compact comme en étendu (leline-throughn'est pas annoncé). Deux clés i18n (state.done,state.locked) ajoutées au libellé suffiraient.DashboardPagen'a aucun test. Cohérent avec le projet (pages non testées sans jsdom), mais c'est la seule logique de la PR qu'aucune de mes mutations ne pouvait atteindre ; un=== 0muté en<= 1passerait vert.onboarding.counters?.transactions: le?.est mort —checklist !== nullimpliquecounters !== nulldans le hook.isLoadingdu tableau de bord : ses liens sont inertes pendant un rechargement des widgets. Conforme à l'issue (qui ne sort que l'état vide du voile), signalé pour mémoire.Actions faites
status:approved(posé par l'auto-revue), inchangé.scratchpad/pr378), aucun fichier du dépôt touché (git statuspropre avant et après).