docs(07): create phase plans for setup wizard

Two plans covering the 3-step first-run wizard: state management + UI
components (Wave 1), then completion logic + routing + redirect (Wave 2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:59:26 +02:00
parent 07823081bb
commit 55eca5dbe1
3 changed files with 857 additions and 5 deletions

View File

@@ -61,11 +61,10 @@ Plans:
3. A "remaining to allocate" balance updates live as items are checked or unchecked in step 2, showing income minus the sum of selected item amounts
4. User can click "Skip" on any individual step or "Skip setup" to exit the wizard entirely without creating any data — and lands on the dashboard
5. Completing the wizard creates a populated template from selected items — user lands on the dashboard with their template in place, and refreshing the browser mid-wizard restores the wizard at the correct step
**Plans:** 3 plans
**Plans:** 2 plans
Plans:
- [x] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
- [x] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
- [x] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
- [ ] 07-01-PLAN.md — Wizard state hook, i18n keys, stepper + step 1/2 UI components (income, recurring items, allocation bar)
- [ ] 07-02-PLAN.md — ReviewStep, completion logic, skip handling, /setup route, first-run redirect, and verification checkpoint
### Phase 8: Auto-Budget Creation
**Goal**: Users never manually trigger budget creation — visiting a month for the first time automatically creates their budget from the template, silently and correctly
@@ -131,6 +130,6 @@ Phases execute in numeric order: 5 → 6 → 7 → 8 → 9
|-------|----------------|--------|-----------|
| 5. Design System Token Rework | 0/3 | Planned | - |
| 6. Preset Data, First-Run Detection, DB Safety | 3/3 | Complete | 2026-04-20 |
| 7. Setup Wizard | 0/? | Not started | - |
| 7. Setup Wizard | 0/2 | Planned | - |
| 8. Auto-Budget Creation | 0/? | Not started | - |
| 9. Inline Add and Dashboard Simplification | 0/? | Not started | - |

View File

@@ -0,0 +1,461 @@
---
phase: 07-setup-wizard
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/pages/SetupPage.tsx
- src/hooks/useWizardState.ts
- 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/i18n/en.json
- src/i18n/de.json
autonomous: true
requirements: [SETUP-01, SETUP-02, SETUP-04]
must_haves:
truths:
- "SetupPage renders a centered card with WizardStepper showing 3 steps"
- "useWizardState persists wizard data to localStorage keyed by userId"
- "IncomeStep shows a number input pre-filled with 3000 and profile currency"
- "RecurringItemsStep shows all 19 PRESETS grouped by type with checkboxes"
- "AllocationBar shows live remaining = income - sum(checked amounts)"
- "Bills and variable_expense items are pre-checked by default"
- "All wizard i18n keys exist in both en.json and de.json"
artifacts:
- path: "src/pages/SetupPage.tsx"
provides: "Wizard page orchestrator with step rendering"
min_lines: 40
- path: "src/hooks/useWizardState.ts"
provides: "localStorage-synced wizard state management"
exports: ["useWizardState"]
- path: "src/components/setup/WizardStepper.tsx"
provides: "Horizontal 1-2-3 stepper bar"
exports: ["WizardStepper"]
- path: "src/components/setup/IncomeStep.tsx"
provides: "Step 1 income input"
exports: ["IncomeStep"]
- path: "src/components/setup/RecurringItemsStep.tsx"
provides: "Step 2 grouped checklist"
exports: ["RecurringItemsStep"]
- path: "src/components/setup/AllocationBar.tsx"
provides: "Sticky remaining balance bar"
exports: ["AllocationBar"]
- path: "src/components/setup/PresetItemRow.tsx"
provides: "Single checkbox row with amount input"
exports: ["PresetItemRow"]
- path: "src/components/setup/CategoryGroupHeader.tsx"
provides: "Section header with colored dot"
exports: ["CategoryGroupHeader"]
key_links:
- from: "src/pages/SetupPage.tsx"
to: "src/hooks/useWizardState.ts"
via: "useWizardState(userId) hook call"
pattern: "useWizardState"
- from: "src/components/setup/RecurringItemsStep.tsx"
to: "src/data/presets.ts"
via: "import PRESETS"
pattern: "import.*PRESETS.*from.*presets"
---
<objective>
Build the setup wizard page shell, state management hook, and all UI components for the 3-step wizard (income, recurring items with live allocation, review placeholder).
Purpose: Establishes the wizard's visual structure, localStorage persistence, and all interactive components. Plan 02 will add the ReviewStep and completion/redirect logic on top of this foundation.
Output: A navigable 3-step wizard at /setup (not yet routed) with working state persistence and live allocation calculation.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export type ItemTier = "fixed" | "variable" | "one_off"
export interface Profile { id: string; display_name: string | null; locale: string; currency: string; setup_completed: boolean; ... }
```
From src/data/presets.ts:
```typescript
export interface PresetItem {
slug: string
type: CategoryType
defaultAmount: number
item_tier: "fixed" | "variable"
}
export const PRESETS: PresetItem[] = [ /* 19 items: 4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment */ ]
```
From src/hooks/useAuth.ts:
```typescript
// Returns { user, profile, loading, signIn, signOut, signUp }
// profile.currency = "EUR" | "USD" | "CHF" etc.
```
From src/hooks/useCategories.ts:
```typescript
export function useCategories(): { categories: Category[]; loading: boolean; create: UseMutationResult; ... }
// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category
```
From src/hooks/useTemplate.ts:
```typescript
export function useTemplate(): { template: Template | null; items: TemplateItem[]; loading: boolean; createItem: UseMutationResult; ... }
// createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Install shadcn checkbox + create useWizardState hook + add i18n keys</name>
<read_first>
- src/i18n/en.json
- src/i18n/de.json
- src/data/presets.ts
- src/hooks/useAuth.ts
</read_first>
<files>src/hooks/useWizardState.ts, src/i18n/en.json, src/i18n/de.json</files>
<action>
1. Install shadcn checkbox:
```bash
npx shadcn@latest add checkbox
```
2. Create `src/hooks/useWizardState.ts`:
```typescript
import { useState, useEffect } from "react"
import { PRESETS } from "@/data/presets"
export interface WizardState {
currentStep: 1 | 2 | 3
income: number
selectedItems: Record<string, { checked: boolean; amount: number }>
}
const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}`
function getDefaultState(): WizardState {
const selectedItems: Record<string, { checked: boolean; amount: number }> = {}
for (const preset of PRESETS) {
selectedItems[preset.slug] = {
checked: preset.type === "bill" || preset.type === "variable_expense",
amount: preset.defaultAmount,
}
}
return { currentStep: 1, income: 3000, selectedItems }
}
export function useWizardState(userId: string) {
const [state, setState] = useState<WizardState>(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY(userId))
if (saved) {
const parsed = JSON.parse(saved)
// Validate shape
if (parsed && typeof parsed.currentStep === "number" && typeof parsed.income === "number" && parsed.selectedItems) {
return parsed as WizardState
}
}
} catch { /* ignore corrupt data */ }
return getDefaultState()
})
useEffect(() => {
localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state))
}, [state, userId])
const setStep = (step: 1 | 2 | 3) => setState(s => ({ ...s, currentStep: step }))
const setIncome = (income: number) => setState(s => ({ ...s, income }))
const toggleItem = (slug: string) => setState(s => ({
...s,
selectedItems: {
...s.selectedItems,
[slug]: { ...s.selectedItems[slug], checked: !s.selectedItems[slug].checked },
},
}))
const setItemAmount = (slug: string, amount: number) => setState(s => ({
...s,
selectedItems: {
...s.selectedItems,
[slug]: { ...s.selectedItems[slug], amount },
},
}))
const clearState = () => {
localStorage.removeItem(STORAGE_KEY(userId))
}
return { state, setStep, setIncome, toggleItem, setItemAmount, clearState }
}
```
3. Add i18n keys to `src/i18n/en.json` — add a `"setup"` object at the top level:
```json
"setup": {
"title": "Set up your budget",
"step1": {
"title": "Monthly Income",
"description": "How much do you earn each month?",
"incomeLabel": "Monthly net income",
"helper": "Enter your total monthly take-home pay",
"validation": "Please enter a positive income amount"
},
"step2": {
"title": "Recurring Items",
"description": "Select your regular monthly expenses",
"remaining": "Remaining to allocate"
},
"step3": {
"title": "Review",
"description": "Confirm your budget template",
"incomeLabel": "Monthly income",
"totalLabel": "Total expenses",
"remainingLabel": "Remaining",
"empty": "No items selected. You can add items to your template later."
},
"steps": {
"1": "Income",
"2": "Items",
"3": "Review"
},
"next": "Next Step",
"back": "Go Back",
"skip": "Skip Step",
"skipSetup": "Skip setup",
"complete": "Complete Setup",
"toast": {
"success": "Template created! Your first budget will appear automatically.",
"error": "Could not save your template. Please try again.",
"partialError": "Some items could not be saved. Check your template page."
}
}
```
4. Add equivalent German keys to `src/i18n/de.json`:
```json
"setup": {
"title": "Budget einrichten",
"step1": {
"title": "Monatliches Einkommen",
"description": "Wie viel verdienst du pro Monat?",
"incomeLabel": "Monatliches Nettoeinkommen",
"helper": "Gib dein monatliches Nettoeinkommen ein",
"validation": "Bitte gib einen positiven Betrag ein"
},
"step2": {
"title": "Wiederkehrende Posten",
"description": "Wahle deine regelmaigen monatlichen Ausgaben",
"remaining": "Verbleibend zu verteilen"
},
"step3": {
"title": "Ubersicht",
"description": "Bestatige deine Budgetvorlage",
"incomeLabel": "Monatliches Einkommen",
"totalLabel": "Gesamtausgaben",
"remainingLabel": "Verbleibend",
"empty": "Keine Posten ausgewahlt. Du kannst spater Posten zu deiner Vorlage hinzufugen."
},
"steps": {
"1": "Einkommen",
"2": "Posten",
"3": "Ubersicht"
},
"next": "Nachster Schritt",
"back": "Zuruck",
"skip": "Schritt uberspringen",
"skipSetup": "Einrichtung uberspringen",
"complete": "Einrichtung abschlieen",
"toast": {
"success": "Vorlage erstellt! Dein erstes Budget wird automatisch erscheinen.",
"error": "Vorlage konnte nicht gespeichert werden. Bitte versuche es erneut.",
"partialError": "Einige Posten konnten nicht gespeichert werden. Prufe deine Vorlagenseite."
}
}
```
</action>
<verify>
<automated>grep -q "useWizardState" src/hooks/useWizardState.ts && grep -q '"setup"' src/i18n/en.json && grep -q '"setup"' src/i18n/de.json && ls src/components/ui/checkbox.tsx</automated>
</verify>
<acceptance_criteria>
- src/hooks/useWizardState.ts contains `export function useWizardState(userId: string)`
- src/hooks/useWizardState.ts contains `setup-wizard-${userId}`
- src/hooks/useWizardState.ts contains `getDefaultState`
- src/i18n/en.json contains `"setup.title"` nested under `"setup": { "title":`
- src/i18n/en.json contains `"setup.toast.success"` with value "Template created!"
- src/i18n/de.json contains `"setup": { "title": "Budget einrichten"`
- src/components/ui/checkbox.tsx exists (shadcn installed)
</acceptance_criteria>
<done>useWizardState hook created with localStorage sync, all setup i18n keys in EN+DE, shadcn checkbox component installed</done>
</task>
<task type="auto">
<name>Task 2: Create all wizard UI components and SetupPage</name>
<read_first>
- src/pages/LoginPage.tsx
- src/components/dashboard/SummaryStrip.tsx
- src/pages/SettingsPage.tsx
- src/data/presets.ts
- src/hooks/useWizardState.ts
- src/components/ui/checkbox.tsx
</read_first>
<files>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</files>
<action>
Create the following components in `src/components/setup/`:
**1. WizardStepper.tsx:**
- Props: `currentStep: 1|2|3`, `onStepClick: (step: 1|2|3) => void`
- Renders 3 circles in a row connected by lines
- Active step: `bg-primary text-primary-foreground` with number (14px/600)
- Completed step (step < currentStep): `bg-primary text-primary-foreground` with lucide `Check` icon (16px)
- Upcoming step (step > currentStep): `bg-muted text-muted-foreground border border-border`
- Connector line between: `h-px w-16` — completed segment `bg-primary`, upcoming `bg-border`
- Step labels below circles: text-xs font-semibold, active `text-foreground`, others `text-muted-foreground`
- Labels from i18n: `t("setup.steps.1")`, `t("setup.steps.2")`, `t("setup.steps.3")`
- Clicking a completed step or current step calls onStepClick. Future steps: no action, cursor-default.
- Wrap in `role="navigation" aria-label="Setup progress"`
- Circle dimensions: `w-8 h-8` (32px)
- Overall layout: `flex items-center justify-center gap-0` with step groups `flex flex-col items-center gap-1` separated by connector lines
- Margin below: handled by parent (mb-6)
**2. IncomeStep.tsx:**
- Props: `income: number`, `onIncomeChange: (val: number) => void`, `currency: string`, `error: string | null`
- Renders card content for step 1
- Label: `t("setup.step1.incomeLabel")` (14px/600)
- Input row: `flex items-center gap-2`
- Input: `type="number"`, `className="w-full text-right text-lg"`, value={income}, onChange converts to number
- Currency suffix: `<span className="text-muted-foreground text-sm">{currency}</span>`
- Helper text: `t("setup.step1.helper")` in `text-sm text-muted-foreground`
- If `error` is truthy: show `<p className="text-sm text-destructive">{error}</p>` below input
- `aria-describedby` on input pointing to helper and error elements
**3. AllocationBar.tsx:**
- Props: `remaining: number`, `currency: string`
- Layout: `sticky top-0 z-10 flex justify-between items-center py-3 px-4 bg-muted border-b border-border rounded-none`
- Left: `<span className="text-sm font-semibold">{t("setup.step2.remaining")}</span>`
- Right: formatted amount using `Intl.NumberFormat` with currency
- `remaining >= 0`: `className="text-base font-semibold text-on-budget"`
- `remaining < 0`: `className="text-base font-semibold text-destructive"`
- Add `aria-live="polite"` on the amount element
**4. CategoryGroupHeader.tsx:**
- Props: `type: CategoryType`, `label: string`, `count: number`
- Layout: `flex items-center gap-2 pt-4 pb-2`
- Colored dot: `w-2.5 h-2.5 rounded-full` with inline style `backgroundColor: var(--color-${type.replace('_', '-')})`
- Map types to CSS vars: income -> `--color-income`, bill -> `--color-bill`, variable_expense -> `--color-variable-expense`, debt -> `--color-debt`, saving -> `--color-saving`, investment -> `--color-investment`
- Label: `<span className="text-sm font-semibold">{label}</span>`
- Count: `<span className="text-xs text-muted-foreground">({count} items)</span>`
- Below: `<Separator />` from shadcn
**5. PresetItemRow.tsx:**
- Props: `slug: string`, `type: CategoryType`, `checked: boolean`, `amount: number`, `onToggle: () => void`, `onAmountChange: (val: number) => void`
- Layout: `flex items-center gap-3 py-2.5 px-1`
- Checkbox: shadcn `<Checkbox checked={checked} onCheckedChange={onToggle} />`
- Item name: `<span className="flex-1 text-sm">{t(\`presets.${type}.${slug}\`)}</span>`
- Category badge: `<Badge variant="outline" className="text-xs" style={{ borderLeftWidth: '3px', borderLeftColor: \`var(--color-${type.replace('_', '-')})\` }}>{t(\`categories.types.${type}\`)}</Badge>`
- Amount input: `<Input type="number" className="w-24 text-right" value={amount} onChange={...} disabled={!checked} />`
- When disabled: add `bg-muted text-muted-foreground opacity-50` classes
**6. RecurringItemsStep.tsx:**
- Props: `selectedItems: Record<string, { checked: boolean; amount: number }>`, `income: number`, `currency: string`, `onToggle: (slug: string) => void`, `onAmountChange: (slug: string, amount: number) => void`
- Groups PRESETS by type using: `Object.groupBy(PRESETS, (p) => p.type)` — if Object.groupBy not available, use manual reduce
- Order groups: `["income", "bill", "variable_expense", "debt", "saving", "investment"]`
- Renders AllocationBar at top (sticky)
- For each group: CategoryGroupHeader + list of PresetItemRow
- Wrap each group in `<fieldset>` with `<legend className="sr-only">{groupLabel}</legend>`
- Compute remaining: `income - sum of all checked items' amounts`
**7. SetupPage.tsx** (page orchestrator):
- Imports useAuth, useWizardState, useTranslation, WizardStepper, IncomeStep, RecurringItemsStep
- Gets userId from `useAuth()` -> `user.id`
- Gets currency from profile: `profile?.currency ?? "EUR"`
- Uses `useWizardState(userId)` for state management
- Layout: `<div className="flex min-h-screen items-center justify-center bg-background p-4"><div className="w-full max-w-2xl">`
- Renders WizardStepper above the card
- Card: `<Card className="w-full border border-border shadow-sm">`
- 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: `<button className="mt-4 text-sm text-muted-foreground underline block mx-auto">{t("setup.skipSetup")}</button>`
- 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)`
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- 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)
</acceptance_criteria>
<done>All 7 wizard UI components render correctly, TypeScript compiles without errors, wizard navigates between steps 1 and 2 with working state persistence</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- `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
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/07-setup-wizard/07-01-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
From src/hooks/useWizardState.ts (created in Plan 01):
```typescript
export interface WizardState {
currentStep: 1 | 2 | 3
income: number
selectedItems: Record<string, { checked: boolean; amount: number }>
}
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ReviewStep + wire completion/skip logic into SetupPage</name>
<read_first>
- src/pages/SetupPage.tsx
- src/hooks/useCategories.ts
- src/hooks/useTemplate.ts
- src/hooks/useWizardState.ts
- src/data/presets.ts
- src/lib/supabase.ts
</read_first>
<files>src/components/setup/ReviewStep.tsx, src/pages/SetupPage.tsx</files>
<action>
**1. Create `src/components/setup/ReviewStep.tsx`:**
Props: `income: number`, `selectedItems: Record<string, { checked: boolean; amount: number }>`, `currency: string`
Layout:
- Income row: `flex justify-between py-2` — left: `t("setup.step3.incomeLabel")` (14px/600), right: formatted income (14px/600)
- `<Separator />`
- 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: `<div className="flex justify-between py-1.5 px-1"><span className="text-sm">{t(\`presets.${type}.${slug}\`)}</span><span className="text-sm text-right">{formattedAmount}</span></div>`
- `<Separator />`
- 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 `<p className="text-sm text-muted-foreground py-4">{t("setup.step3.empty")}</p>` 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 `<ReviewStep income={state.income} selectedItems={state.selectedItems} currency={currency} />`
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<string, string> = {}
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).
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Register /setup route in App.tsx + add first-run redirect in DashboardPage</name>
<read_first>
- src/App.tsx
- src/pages/DashboardPage.tsx
- src/hooks/useFirstRunState.ts
</read_first>
<files>src/App.tsx, src/pages/DashboardPage.tsx</files>
<action>
**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
<Route
path="/setup"
element={
<ProtectedRoute>
<SetupPage />
</ProtectedRoute>
}
/>
```
Insert this between the `</Route>` closing the register route (around line 46) and the `<Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>` 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 <DashboardSkeleton />
if (isFirstRun) return <Navigate to="/setup" replace />
```
This uses the existing `DashboardSkeleton` component (already imported) as the loading fallback.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- src/App.tsx contains `import SetupPage from "@/pages/SetupPage"`
- src/App.tsx contains `path="/setup"` within a `<ProtectedRoute>` 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 <Navigate to="/setup" replace />`
- src/pages/DashboardPage.tsx contains `if (firstRunLoading) return <DashboardSkeleton />`
- TypeScript compiles without errors
</acceptance_criteria>
<done>/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.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>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.</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/07-setup-wizard/07-02-SUMMARY.md`
</output>