The checklist state moves from DashboardPage to OnboardingProvider, mounted
in AppShell around the routed page; pages read it through useOnboarding().
The reducer stays in useOnboardingChecklist:
- loads are numbered and a stale answer is ignored;
- HIDE, SHOW, SKIP and RESUME win over the snapshot of a load started
before them (#384);
- a refresh keeps the counters and the ready status, and a failed re-read
after a first success keeps the last state; only the start of a failure
streak is logged;
- re-read at mount, on every route change and 150 ms after a burst of
data events.
onboardingService: getOnboardingSnapshot replaces getOnboardingCounters and
is a pure read (skipped steps included, one preference per step, filtered on
the six keys and the value "1"); initializeGuidedPath applies rule D9 once
per profile in one transaction (hidden first, then the flag, ROLLBACK on
failure), awaited once by the first load through a promise shared by
StrictMode's double mount; skipStep / resumeStep are idempotent.
computeChecklist counts skipped rows; the new pure onboardingPath module
holds nextStep, CARD_PAGES (empty for now), stepsOfPage, the card latch,
bannerMode and readOnboardingNavState. The dashboard reads the context and
keeps its compact bar for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile without any transaction now opens a dashboard that says what to
do: a welcome card with the expanded getting-started checklist and one
primary action towards the import page, instead of widgets showing 0 $.
With transactions, the compact checklist sits between the title and the
filter panel until it is complete or dismissed.
- `EmptyState` (pure, dashed card from the import source list; a `to`
action renders a react-router `<Link>`, an `onClick` action a button)
- `OnboardingChecklist` (pure, expanded and compact modes; undone rows
link to their route, the locked Budget row shows a lock, the "Base and
up" label and links to the license card; neutral budget row and hidden
counter until the entitlement is known)
- `DashboardEmptyState` composes both
- `DashboardPage` branches on the hook `status`: loading renders the title,
help and banner only; error falls back to the v0.15.0 page without the
list and logs a warning; ready with zero transactions renders the empty
state outside the `isLoading` veil
- `useOnboardingChecklist` exposes `counters` and `error` for the page
- Users settings: "Show the getting-started checklist" button in the Guide
section, visible only when the preference is set, with an inline
confirmation after the click
- FR + EN keys `dashboard.empty.*`, `onboarding.checklist.*`,
`settings.users.onboarding.*`
Tests: `EmptyState.test.tsx` and `OnboardingChecklist.test.tsx` by direct
call (FilterPanel pattern). No DB migration. CHANGELOG left to #357.
Resolves#352
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Lay the data foundation of the getting-started checklist, without UI:
- transactionService.getTransactionCount(): unfiltered COUNT(*) of transactions
- onboardingService: getOnboardingCounters() reads the five aggregated counts,
the import folder and the onboarding_checklist_hidden preference in one
Promise.all; hideChecklist()/showChecklist() persist the preference and log
- utils/onboardingChecklist.computeChecklist(): pure computation of the six
rows (profile, importFolder, source, import, manualCategory, budget), with
the budget row locked and excluded from the total when the edition has no
Budget
- hooks/useOnboardingChecklist: exported reducer + initial state, actions
refresh/hide/show, composed with useEntitlement("budget"); a load error
sets status "error" and keeps the hidden flag
- types OnboardingCounters, ChecklistItem, ChecklistItemKey
Tests cover every row in isolation, the budget gating (total 5 vs 6, complete
at 5/5 locked and 6/6 open), the aggregated query and preference reads, and
every reducer action. No DB migration, no CHANGELOG entry (centralised in #357).
Resolves#351
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>