Files
SimpleFinanceDash/.planning/phases/07-setup-wizard/07-01-PLAN.md
Jean-Luc Makiola 55eca5dbe1 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>
2026-04-20 20:59:26 +02:00

462 lines
20 KiB
Markdown

---
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>