Files
SimpleFinanceDash/.planning/phases/07-setup-wizard/07-02-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

17 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 02 execute 2
07-01
src/components/setup/ReviewStep.tsx
src/pages/SetupPage.tsx
src/App.tsx
src/pages/DashboardPage.tsx
true
SETUP-01
SETUP-03
SETUP-05
truths artifacts key_links
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
path provides exports
src/components/setup/ReviewStep.tsx Read-only summary of wizard selections
ReviewStep
path provides contains
src/App.tsx /setup route registration path="/setup"
path provides contains
src/pages/DashboardPage.tsx First-run redirect to /setup useFirstRunState
from to via pattern
src/pages/DashboardPage.tsx src/hooks/useFirstRunState.ts useFirstRunState() -> isFirstRun -> Navigate to /setup isFirstRun.*Navigate.*setup
from to via pattern
src/pages/SetupPage.tsx src/hooks/useCategories.ts create.mutateAsync on wizard completion create.mutateAsync
from to via pattern
src/pages/SetupPage.tsx src/hooks/useTemplate.ts createItem.mutateAsync on wizard completion createItem.mutateAsync
from to via pattern
src/App.tsx src/pages/SetupPage.tsx Route path=/setup element=SetupPage 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.

<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 @.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:

// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category { id, ... }

From src/hooks/useTemplate.ts:

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

export function useFirstRunState(): { isFirstRun: boolean; loading: boolean }

From src/data/presets.ts:

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<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}`)}{formattedAmount}`
  • <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:

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:

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

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:

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

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

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

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

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