`
+- Renders WizardStepper above the card
+- Card: `
`
+ - CardHeader: step title (heading 20px/600) + step description (body 14px/400 text-muted-foreground)
+ - CardContent: renders active step component based on `state.currentStep`
+ - Bottom nav: `flex justify-between items-center pt-4 border-t border-border`
+ - Left side: Go Back button (variant="ghost", hidden on step 1) + Skip Step button (variant="ghost" text-muted-foreground)
+ - Right side: Next Step button (variant="default")
+- Step 1 Next: validates income > 0, shows error if invalid, otherwise advances to step 2
+- Step 2 Next: no validation, advances to step 3
+- Step 3: shows placeholder text "Review step coming in next plan" (will be replaced in Plan 02)
+- Below card: "Skip setup" link: ``
+- Skip step on step 1: advance to step 2. Skip step on step 2: advance to step 3.
+- Clicking completed stepper steps calls `setStep(step)`
+
+
+ ls src/components/setup/WizardStepper.tsx src/components/setup/IncomeStep.tsx src/components/setup/AllocationBar.tsx src/components/setup/CategoryGroupHeader.tsx src/components/setup/PresetItemRow.tsx src/components/setup/RecurringItemsStep.tsx src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -30
+
+
+ - src/pages/SetupPage.tsx contains `useWizardState` and `max-w-2xl`
+ - src/pages/SetupPage.tsx contains `currentStep` conditional rendering
+ - src/components/setup/WizardStepper.tsx contains `role="navigation"` and `aria-label`
+ - src/components/setup/WizardStepper.tsx contains `w-8 h-8`
+ - src/components/setup/IncomeStep.tsx contains `type="number"` and `text-lg`
+ - src/components/setup/AllocationBar.tsx contains `aria-live="polite"` and `sticky top-0`
+ - src/components/setup/CategoryGroupHeader.tsx contains `w-2.5 h-2.5 rounded-full`
+ - src/components/setup/PresetItemRow.tsx contains `Checkbox` import and `w-24`
+ - src/components/setup/RecurringItemsStep.tsx imports `PRESETS` from `@/data/presets`
+ - TypeScript compiles without errors (`npx tsc --noEmit` exits 0)
+
+ All 7 wizard UI components render correctly, TypeScript compiles without errors, wizard navigates between steps 1 and 2 with working state persistence
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| localStorage -> component | Persisted wizard data read back into React state |
+| client -> Supabase (future Plan 02) | Category/template item creation on completion |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-07-01 | Tampering | useWizardState localStorage read | mitigate | Validate JSON shape on load: check currentStep is 1-3, income is number, selectedItems is object with expected structure. Fall back to defaults on invalid data. |
+| T-07-02 | Information Disclosure | localStorage key | accept | Key includes userId, preventing cross-user data leakage. Data is non-sensitive (income amount, item selections). |
+| T-07-03 | Spoofing | Preset data | accept | PRESETS are hardcoded source constants, not user input. i18n keys resolve from bundled JSON, no injection vector. |
+
+
+
+- `npx tsc --noEmit` passes (no type errors)
+- All 7 component files exist in src/components/setup/ and src/pages/
+- useWizardState stores and retrieves state from localStorage
+- i18n keys resolve for both "en" and "de" locales
+
+
+
+- SetupPage renders with WizardStepper showing 3 steps
+- Step 1 shows income input pre-filled with 3000 and currency suffix
+- Step 2 shows all 19 PRESETS grouped by type with checkboxes and editable amounts
+- Bills (4) and variable_expense (5) items are checked by default
+- AllocationBar shows remaining = 3000 - sum(checked) and turns red when negative
+- Navigating between steps preserves state
+- Refreshing the page restores wizard at correct step (localStorage)
+
+
+
diff --git a/.planning/phases/07-setup-wizard/07-02-PLAN.md b/.planning/phases/07-setup-wizard/07-02-PLAN.md
new file mode 100644
index 0000000..a6155d2
--- /dev/null
+++ b/.planning/phases/07-setup-wizard/07-02-PLAN.md
@@ -0,0 +1,392 @@
+---
+phase: 07-setup-wizard
+plan: 02
+type: execute
+wave: 2
+depends_on: [07-01]
+files_modified:
+ - src/components/setup/ReviewStep.tsx
+ - src/pages/SetupPage.tsx
+ - src/App.tsx
+ - src/pages/DashboardPage.tsx
+autonomous: true
+requirements: [SETUP-01, SETUP-03, SETUP-05]
+
+must_haves:
+ truths:
+ - "ReviewStep shows read-only summary of income + selected items grouped by type + totals"
+ - "/setup route is protected and renders SetupPage outside AppLayout"
+ - "DashboardPage redirects first-run users to /setup via useFirstRunState"
+ - "Completing wizard creates categories then template items and redirects to dashboard"
+ - "Skip setup clears localStorage, marks setup_completed=true, redirects to dashboard"
+ - "Complete button is disabled during API calls to prevent double-submit"
+ artifacts:
+ - path: "src/components/setup/ReviewStep.tsx"
+ provides: "Read-only summary of wizard selections"
+ exports: ["ReviewStep"]
+ - path: "src/App.tsx"
+ provides: "/setup route registration"
+ contains: "path=\"/setup\""
+ - path: "src/pages/DashboardPage.tsx"
+ provides: "First-run redirect to /setup"
+ contains: "useFirstRunState"
+ key_links:
+ - from: "src/pages/DashboardPage.tsx"
+ to: "src/hooks/useFirstRunState.ts"
+ via: "useFirstRunState() -> isFirstRun -> Navigate to /setup"
+ pattern: "isFirstRun.*Navigate.*setup"
+ - from: "src/pages/SetupPage.tsx"
+ to: "src/hooks/useCategories.ts"
+ via: "create.mutateAsync on wizard completion"
+ pattern: "create\\.mutateAsync"
+ - from: "src/pages/SetupPage.tsx"
+ to: "src/hooks/useTemplate.ts"
+ via: "createItem.mutateAsync on wizard completion"
+ pattern: "createItem\\.mutateAsync"
+ - from: "src/App.tsx"
+ to: "src/pages/SetupPage.tsx"
+ via: "Route path=/setup element=SetupPage"
+ pattern: "path=.*/setup"
+---
+
+
+Add the ReviewStep component, wire wizard completion logic (creating categories + template items), add skip functionality, register the /setup route, and add first-run redirect in DashboardPage.
+
+Purpose: Completes the wizard end-to-end — a new user is redirected to /setup, can navigate all 3 steps, and either completes (creating template data) or skips.
+Output: Fully functional setup wizard accessible via /setup with working completion and skip flows.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/07-setup-wizard/07-CONTEXT.md
+@.planning/phases/07-setup-wizard/07-RESEARCH.md
+@.planning/phases/07-setup-wizard/07-PATTERNS.md
+@.planning/phases/07-setup-wizard/07-UI-SPEC.md
+@.planning/phases/07-setup-wizard/07-01-SUMMARY.md
+
+
+From src/hooks/useWizardState.ts (created in Plan 01):
+```typescript
+export interface WizardState {
+ currentStep: 1 | 2 | 3
+ income: number
+ selectedItems: Record
+}
+export function useWizardState(userId: string): {
+ state: WizardState
+ setStep: (step: 1 | 2 | 3) => void
+ setIncome: (income: number) => void
+ toggleItem: (slug: string) => void
+ setItemAmount: (slug: string, amount: number) => void
+ clearState: () => void
+}
+```
+
+From src/hooks/useCategories.ts:
+```typescript
+// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category { id, ... }
+```
+
+From src/hooks/useTemplate.ts:
+```typescript
+// createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem
+// Template auto-creates on first useTemplate() query
+```
+
+From src/hooks/useFirstRunState.ts:
+```typescript
+export function useFirstRunState(): { isFirstRun: boolean; loading: boolean }
+```
+
+From src/data/presets.ts:
+```typescript
+export const PRESETS: PresetItem[] // 19 items with slug, type, defaultAmount, item_tier
+```
+
+
+
+
+
+
+ Task 1: Create ReviewStep + wire completion/skip logic into SetupPage
+
+ - src/pages/SetupPage.tsx
+ - src/hooks/useCategories.ts
+ - src/hooks/useTemplate.ts
+ - src/hooks/useWizardState.ts
+ - src/data/presets.ts
+ - src/lib/supabase.ts
+
+ src/components/setup/ReviewStep.tsx, src/pages/SetupPage.tsx
+
+**1. Create `src/components/setup/ReviewStep.tsx`:**
+
+Props: `income: number`, `selectedItems: Record`, `currency: string`
+
+Layout:
+- Income row: `flex justify-between py-2` — left: `t("setup.step3.incomeLabel")` (14px/600), right: formatted income (14px/600)
+- ``
+- Group checked items by type (same order: income, bill, variable_expense, debt, saving, investment). Only show groups that have at least one checked item.
+- For each group: render CategoryGroupHeader (import from `./CategoryGroupHeader`), then for each checked item in group: `{t(\`presets.${type}.${slug}\`)}{formattedAmount}
`
+- ``
+- Totals: `space-y-1 pt-2`
+ - Total expenses row: `flex justify-between` with label `t("setup.step3.totalLabel")` (14px/600) and sum of checked amounts
+ - Remaining row: `flex justify-between` with label `t("setup.step3.remainingLabel")` (16px/600), value colored `text-on-budget` if >= 0, `text-destructive` if < 0
+- If no items checked: show `{t("setup.step3.empty")}
` instead of groups
+
+Use `Intl.NumberFormat` with `style: "currency"` and the `currency` prop for all amount formatting.
+
+Import PRESETS from `@/data/presets` to map slugs to types for grouping.
+
+**2. Update `src/pages/SetupPage.tsx` to add:**
+
+a) Import ReviewStep, useCategories, useTemplate, useQueryClient, toast (from sonner), supabase (from @/lib/supabase), Navigate (from react-router-dom), useNavigate
+
+b) Add `completing` state: `const [completing, setCompleting] = useState(false)`
+
+c) Replace the step 3 placeholder with ``
+
+d) Change the right-side button on step 3 from "Next Step" to "Complete Setup" (`t("setup.complete")`), disabled when `completing`
+
+e) Add completion handler `handleComplete`:
+```typescript
+async function handleComplete() {
+ setCompleting(true)
+ try {
+ const queryClient = useQueryClient() // already declared at top
+ const checkedItems = PRESETS.filter(p => state.selectedItems[p.slug]?.checked)
+
+ // 1. Determine unique category types needed
+ const typesNeeded = [...new Set(checkedItems.map(i => i.type))]
+
+ // 2. Create one category per type (use i18n label as name)
+ const categoryMap: Record = {}
+ for (const type of typesNeeded) {
+ try {
+ const cat = await create.mutateAsync({ name: t(`categories.types.${type}`), type })
+ categoryMap[type] = cat.id
+ } catch (e: any) {
+ // Unique constraint violation (23505) = category already exists, fetch it
+ if (e?.code === "23505" || e?.message?.includes("duplicate")) {
+ const existing = categories.find(c => c.type === type)
+ if (existing) categoryMap[type] = existing.id
+ } else {
+ throw e
+ }
+ }
+ }
+
+ // 3. If categories were fetched from cache but not found, refetch
+ if (Object.keys(categoryMap).length < typesNeeded.length) {
+ await queryClient.invalidateQueries({ queryKey: ["categories"] })
+ // Remaining types need fresh data — handled by the existing entries
+ }
+
+ // 4. Create template items for each checked preset
+ let partialFailure = false
+ for (const item of checkedItems) {
+ try {
+ await createItem.mutateAsync({
+ category_id: categoryMap[item.type],
+ item_tier: item.item_tier,
+ budgeted_amount: state.selectedItems[item.slug].amount,
+ })
+ } catch {
+ partialFailure = true
+ }
+ }
+
+ // 5. Mark setup complete
+ await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id)
+
+ // 6. Clear wizard state
+ clearState()
+
+ // 7. Invalidate queries to prevent redirect loop
+ await queryClient.invalidateQueries({ queryKey: ["categories"] })
+ await queryClient.invalidateQueries({ queryKey: ["template-items"] })
+
+ // 8. Toast and redirect
+ if (partialFailure) {
+ toast.error(t("setup.toast.partialError"))
+ } else {
+ toast.success(t("setup.toast.success"))
+ }
+ navigate("/", { replace: true })
+ } catch {
+ toast.error(t("setup.toast.error"))
+ } finally {
+ setCompleting(false)
+ }
+}
+```
+
+f) Add skip handler `handleSkipSetup`:
+```typescript
+async function handleSkipSetup() {
+ clearState()
+ await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id)
+ navigate("/", { replace: true })
+}
+```
+
+g) Wire "Skip setup" button below card to call `handleSkipSetup`
+
+h) Wire "Skip Step" on step 3 to also call `handleSkipSetup` (per UI-SPEC: "On step 3 skip: same as Skip setup")
+
+i) Disable Go Back, Skip Step, and Complete Setup buttons when `completing` is true
+
+j) Add `useCategories()` call at component top level to get `create` mutation and `categories` array. Add `useTemplate()` to ensure template row exists before completion (Pitfall 5).
+
+k) Get `useQueryClient()` at component top level (not inside handler).
+
+
+ grep -q "ReviewStep" src/components/setup/ReviewStep.tsx && grep -q "handleComplete" src/pages/SetupPage.tsx && grep -q "setup_completed.*true" src/pages/SetupPage.tsx && grep -q "clearState" src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -20
+
+
+ - src/components/setup/ReviewStep.tsx contains `t("setup.step3.incomeLabel")` and `Intl.NumberFormat`
+ - src/components/setup/ReviewStep.tsx contains `text-on-budget` and `text-destructive` conditional
+ - src/pages/SetupPage.tsx contains `async function handleComplete`
+ - src/pages/SetupPage.tsx contains `create.mutateAsync` (category creation)
+ - src/pages/SetupPage.tsx contains `createItem.mutateAsync` (template item creation)
+ - src/pages/SetupPage.tsx contains `setup_completed: true` (profile update)
+ - src/pages/SetupPage.tsx contains `handleSkipSetup` function
+ - src/pages/SetupPage.tsx contains `setCompleting(true)` (double-click prevention)
+ - src/pages/SetupPage.tsx contains `toast.success` and `toast.error`
+ - src/pages/SetupPage.tsx contains `invalidateQueries.*categories` (redirect loop prevention)
+ - TypeScript compiles without errors
+
+ ReviewStep renders read-only grouped summary. Completion creates categories + template items, handles duplicates, clears state, marks setup_completed, toasts, and redirects. Skip exits without creating data.
+
+
+
+ Task 2: Register /setup route in App.tsx + add first-run redirect in DashboardPage
+
+ - src/App.tsx
+ - src/pages/DashboardPage.tsx
+ - src/hooks/useFirstRunState.ts
+
+ src/App.tsx, src/pages/DashboardPage.tsx
+
+**1. Update `src/App.tsx`:**
+
+Add import at top:
+```typescript
+import SetupPage from "@/pages/SetupPage"
+```
+
+Add the /setup route AFTER the public routes (login, register) and BEFORE the AppLayout route. It must be inside a ProtectedRoute but NOT inside AppLayout:
+```typescript
+
+
+
+ }
+/>
+```
+
+Insert this between the `` closing the register route (around line 46) and the `}>` line (around line 47).
+
+**2. Update `src/pages/DashboardPage.tsx`:**
+
+Add imports at top (after existing imports):
+```typescript
+import { useFirstRunState } from "@/hooks/useFirstRunState"
+import { Navigate } from "react-router-dom"
+```
+
+Note: `Navigate` may already be imported — check before adding a duplicate.
+
+Inside the DashboardPage component function, add early returns BEFORE any existing logic (before the existing useState/useMemo calls):
+```typescript
+const { isFirstRun, loading: firstRunLoading } = useFirstRunState()
+
+if (firstRunLoading) return
+if (isFirstRun) return
+```
+
+This uses the existing `DashboardSkeleton` component (already imported) as the loading fallback.
+
+
+ grep -q 'path="/setup"' src/App.tsx && grep -q "SetupPage" src/App.tsx && grep -q "useFirstRunState" src/pages/DashboardPage.tsx && grep -q 'Navigate to="/setup"' src/pages/DashboardPage.tsx && npx tsc --noEmit 2>&1 | head -20
+
+
+ - src/App.tsx contains `import SetupPage from "@/pages/SetupPage"`
+ - src/App.tsx contains `path="/setup"` within a `` wrapper
+ - src/App.tsx has /setup route BEFORE the AppLayout route (not nested inside it)
+ - src/pages/DashboardPage.tsx contains `import { useFirstRunState } from "@/hooks/useFirstRunState"`
+ - src/pages/DashboardPage.tsx contains `const { isFirstRun, loading: firstRunLoading } = useFirstRunState()`
+ - src/pages/DashboardPage.tsx contains `if (isFirstRun) return `
+ - src/pages/DashboardPage.tsx contains `if (firstRunLoading) return `
+ - TypeScript compiles without errors
+
+ /setup route registered as protected standalone page. First-run users are redirected from dashboard to /setup. Existing users with categories/template data see the normal dashboard.
+
+
+
+ Complete 3-step setup wizard with income entry, recurring items checklist (19 presets), review step, completion logic (category + template creation), skip functionality, and first-run redirect from dashboard.
+
+ 1. Create a new test user (or clear an existing user's categories and template items in Supabase)
+ 2. Log in with the test user — should be redirected to `/setup`
+ 3. Step 1: Verify income input shows 3000 pre-filled with EUR suffix. Try Next with empty/0 value (should show validation error). Enter 4000, click Next.
+ 4. Step 2: Verify Bills (4) and Variable Expenses (5) are pre-checked. Verify allocation bar shows "Remaining to allocate: X" (4000 - sum of checked). Uncheck an item — verify amount updates. Check an item — verify it enables the amount input.
+ 5. Step 3: Verify read-only summary shows income, grouped checked items, totals, and remaining.
+ 6. Click "Complete Setup" — verify toast appears, redirected to dashboard, template page shows created items.
+ 7. Refresh dashboard — should NOT redirect back to /setup (setup_completed=true, and categories exist).
+ 8. Test "Skip setup" link — create another fresh user, click "Skip setup" from step 1. Verify redirect to dashboard with no data created.
+ 9. Test localStorage persistence: start wizard, enter income, advance to step 2, refresh page — verify wizard resumes at step 2 with entered income.
+
+ Type "approved" or describe any issues found
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| client -> Supabase | Category and template item inserts on completion |
+| localStorage -> component | Wizard state restoration (validated in Plan 01) |
+| React Query cache -> redirect logic | isFirstRun depends on cache freshness |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-07-04 | Denial of Service | handleComplete double-click | mitigate | `completing` state disables button immediately on click. DB unique constraints on categories prevent duplicate rows. |
+| T-07-05 | Tampering | Supabase profile update (setup_completed) | accept | RLS policy restricts updates to own profile row. Client can only set own setup_completed. |
+| T-07-06 | Elevation of Privilege | /setup route access | mitigate | Route wrapped in ProtectedRoute — unauthenticated users redirected to /login. |
+| T-07-07 | Denial of Service | Redirect loop (dashboard <-> /setup) | mitigate | After completion, invalidate ["categories"] and ["template-items"] queries before redirect. useFirstRunState will read fresh data showing isFirstRun=false. |
+
+
+
+- `npx tsc --noEmit` passes
+- Navigating to /setup while unauthenticated redirects to /login
+- A user with zero categories sees /setup after login
+- Completing wizard creates categories + template items in database
+- Skipping wizard sets setup_completed=true without creating data
+- Refreshing mid-wizard restores state from localStorage
+- No redirect loop after completion
+
+
+
+- New user flow: login -> auto-redirect to /setup -> 3 steps -> complete -> dashboard with template
+- Skip flow: /setup -> skip -> dashboard (no data created, setup_completed=true)
+- Existing user flow: login -> dashboard (no redirect to /setup)
+- No duplicate categories on repeated completion attempts
+- No infinite redirect loop between dashboard and /setup
+
+
+