From 55eca5dbe12d75c1ab39f5faa76d3134b8fff6dc Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Apr 2026 20:59:26 +0200 Subject: [PATCH] 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 --- .planning/ROADMAP.md | 9 +- .../phases/07-setup-wizard/07-01-PLAN.md | 461 ++++++++++++++++++ .../phases/07-setup-wizard/07-02-PLAN.md | 392 +++++++++++++++ 3 files changed, 857 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/07-setup-wizard/07-01-PLAN.md create mode 100644 .planning/phases/07-setup-wizard/07-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 5a8f903..7837867 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 | - | diff --git a/.planning/phases/07-setup-wizard/07-01-PLAN.md b/.planning/phases/07-setup-wizard/07-01-PLAN.md new file mode 100644 index 0000000..3187234 --- /dev/null +++ b/.planning/phases/07-setup-wizard/07-01-PLAN.md @@ -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" +--- + + +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. + + + +@$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 + + +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 +``` + + + + + + + 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 +``` + +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 +} + +const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}` + +function getDefaultState(): WizardState { + const selectedItems: Record = {} + 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(() => { + 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." + } +} +``` + + + 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: `{currency}` +- Helper text: `t("setup.step1.helper")` in `text-sm text-muted-foreground` +- If `error` is truthy: show `

{error}

` 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: `{t("setup.step2.remaining")}` +- 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: `{label}` +- Count: `({count} items)` +- Below: `` 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 `` +- Item name: `{t(\`presets.${type}.${slug}\`)}` +- Category badge: `{t(\`categories.types.${type}\`)}` +- Amount input: `` + - When disabled: add `bg-muted text-muted-foreground opacity-50` classes + +**6. RecurringItemsStep.tsx:** +- Props: `selectedItems: Record`, `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 `
` with `{groupLabel}` +- 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: `
` +- 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) + + + +After completion, create `.planning/phases/07-setup-wizard/07-01-SUMMARY.md` + 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 + + + +After completion, create `.planning/phases/07-setup-wizard/07-02-SUMMARY.md` +