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

20 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-setup-wizard 01 execute 1
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
true
SETUP-01
SETUP-02
SETUP-04
truths artifacts key_links
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
path provides min_lines
src/pages/SetupPage.tsx Wizard page orchestrator with step rendering 40
path provides exports
src/hooks/useWizardState.ts localStorage-synced wizard state management
useWizardState
path provides exports
src/components/setup/WizardStepper.tsx Horizontal 1-2-3 stepper bar
WizardStepper
path provides exports
src/components/setup/IncomeStep.tsx Step 1 income input
IncomeStep
path provides exports
src/components/setup/RecurringItemsStep.tsx Step 2 grouped checklist
RecurringItemsStep
path provides exports
src/components/setup/AllocationBar.tsx Sticky remaining balance bar
AllocationBar
path provides exports
src/components/setup/PresetItemRow.tsx Single checkbox row with amount input
PresetItemRow
path provides exports
src/components/setup/CategoryGroupHeader.tsx Section header with colored dot
CategoryGroupHeader
from to via pattern
src/pages/SetupPage.tsx src/hooks/useWizardState.ts useWizardState(userId) hook call useWizardState
from to via pattern
src/components/setup/RecurringItemsStep.tsx src/data/presets.ts import PRESETS import.*PRESETS.*from.*presets
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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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:

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:

// Returns { user, profile, loading, signIn, signOut, signUp }
// profile.currency = "EUR" | "USD" | "CHF" etc.

From src/hooks/useCategories.ts:

export function useCategories(): { categories: Category[]; loading: boolean; create: UseMutationResult; ... }
// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category

From src/hooks/useTemplate.ts:

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
Task 1: Install shadcn checkbox + create useWizardState hook + add i18n keys - src/i18n/en.json - src/i18n/de.json - src/data/presets.ts - src/hooks/useAuth.ts src/hooks/useWizardState.ts, src/i18n/en.json, src/i18n/de.json 1. Install shadcn checkbox: ```bash npx shadcn@latest add checkbox ```
  1. Create src/hooks/useWizardState.ts:
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 }
}
  1. Add i18n keys to src/i18n/en.json — add a "setup" object at the top level:
"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."
  }
}
  1. Add equivalent German keys to src/i18n/de.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."
  }
}
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 - 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) useWizardState hook created with localStorage sync, all setup i18n keys in EN+DE, shadcn checkbox component installed Task 2: Create all wizard UI components and SetupPage - 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 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 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}`)}`
  • Category badge: <Badge variant="outline" className="text-xs" style={{ borderLeftWidth: '3px', borderLeftColor: \var(--color-${type.replace('_', '-')})` }}>{t(`categories.types.${type}`)}`
  • 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) 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 <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> All 7 wizard UI components render correctly, TypeScript compiles without errors, wizard navigates between steps 1 and 2 with working state persistence

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

<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>
After completion, create `.planning/phases/07-setup-wizard/07-01-SUMMARY.md`