docs(07): create phase plans for setup wizard
Two plans covering the 3-step first-run wizard: state management + UI components (Wave 1), then completion logic + routing + redirect (Wave 2). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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
|
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
|
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:
|
Plans:
|
||||||
- [x] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
|
- [ ] 07-01-PLAN.md — Wizard state hook, i18n keys, stepper + step 1/2 UI components (income, recurring items, allocation bar)
|
||||||
- [x] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
|
- [ ] 07-02-PLAN.md — ReviewStep, completion logic, skip handling, /setup route, first-run redirect, and verification checkpoint
|
||||||
- [x] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
|
|
||||||
|
|
||||||
### Phase 8: Auto-Budget Creation
|
### 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
|
**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 | - |
|
| 5. Design System Token Rework | 0/3 | Planned | - |
|
||||||
| 6. Preset Data, First-Run Detection, DB Safety | 3/3 | Complete | 2026-04-20 |
|
| 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 | - |
|
| 8. Auto-Budget Creation | 0/? | Not started | - |
|
||||||
| 9. Inline Add and Dashboard Simplification | 0/? | Not started | - |
|
| 9. Inline Add and Dashboard Simplification | 0/? | Not started | - |
|
||||||
|
|||||||
461
.planning/phases/07-setup-wizard/07-01-PLAN.md
Normal file
461
.planning/phases/07-setup-wizard/07-01-PLAN.md
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
---
|
||||||
|
phase: 07-setup-wizard
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/pages/SetupPage.tsx
|
||||||
|
- src/hooks/useWizardState.ts
|
||||||
|
- src/components/setup/WizardStepper.tsx
|
||||||
|
- src/components/setup/IncomeStep.tsx
|
||||||
|
- src/components/setup/AllocationBar.tsx
|
||||||
|
- src/components/setup/CategoryGroupHeader.tsx
|
||||||
|
- src/components/setup/PresetItemRow.tsx
|
||||||
|
- src/components/setup/RecurringItemsStep.tsx
|
||||||
|
- src/i18n/en.json
|
||||||
|
- src/i18n/de.json
|
||||||
|
autonomous: true
|
||||||
|
requirements: [SETUP-01, SETUP-02, SETUP-04]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "SetupPage renders a centered card with WizardStepper showing 3 steps"
|
||||||
|
- "useWizardState persists wizard data to localStorage keyed by userId"
|
||||||
|
- "IncomeStep shows a number input pre-filled with 3000 and profile currency"
|
||||||
|
- "RecurringItemsStep shows all 19 PRESETS grouped by type with checkboxes"
|
||||||
|
- "AllocationBar shows live remaining = income - sum(checked amounts)"
|
||||||
|
- "Bills and variable_expense items are pre-checked by default"
|
||||||
|
- "All wizard i18n keys exist in both en.json and de.json"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/pages/SetupPage.tsx"
|
||||||
|
provides: "Wizard page orchestrator with step rendering"
|
||||||
|
min_lines: 40
|
||||||
|
- path: "src/hooks/useWizardState.ts"
|
||||||
|
provides: "localStorage-synced wizard state management"
|
||||||
|
exports: ["useWizardState"]
|
||||||
|
- path: "src/components/setup/WizardStepper.tsx"
|
||||||
|
provides: "Horizontal 1-2-3 stepper bar"
|
||||||
|
exports: ["WizardStepper"]
|
||||||
|
- path: "src/components/setup/IncomeStep.tsx"
|
||||||
|
provides: "Step 1 income input"
|
||||||
|
exports: ["IncomeStep"]
|
||||||
|
- path: "src/components/setup/RecurringItemsStep.tsx"
|
||||||
|
provides: "Step 2 grouped checklist"
|
||||||
|
exports: ["RecurringItemsStep"]
|
||||||
|
- path: "src/components/setup/AllocationBar.tsx"
|
||||||
|
provides: "Sticky remaining balance bar"
|
||||||
|
exports: ["AllocationBar"]
|
||||||
|
- path: "src/components/setup/PresetItemRow.tsx"
|
||||||
|
provides: "Single checkbox row with amount input"
|
||||||
|
exports: ["PresetItemRow"]
|
||||||
|
- path: "src/components/setup/CategoryGroupHeader.tsx"
|
||||||
|
provides: "Section header with colored dot"
|
||||||
|
exports: ["CategoryGroupHeader"]
|
||||||
|
key_links:
|
||||||
|
- from: "src/pages/SetupPage.tsx"
|
||||||
|
to: "src/hooks/useWizardState.ts"
|
||||||
|
via: "useWizardState(userId) hook call"
|
||||||
|
pattern: "useWizardState"
|
||||||
|
- from: "src/components/setup/RecurringItemsStep.tsx"
|
||||||
|
to: "src/data/presets.ts"
|
||||||
|
via: "import PRESETS"
|
||||||
|
pattern: "import.*PRESETS.*from.*presets"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Build the setup wizard page shell, state management hook, and all UI components for the 3-step wizard (income, recurring items with live allocation, review placeholder).
|
||||||
|
|
||||||
|
Purpose: Establishes the wizard's visual structure, localStorage persistence, and all interactive components. Plan 02 will add the ReviewStep and completion/redirect logic on top of this foundation.
|
||||||
|
Output: A navigable 3-step wizard at /setup (not yet routed) with working state persistence and live allocation calculation.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-CONTEXT.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-RESEARCH.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-PATTERNS.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-UI-SPEC.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
From src/lib/types.ts:
|
||||||
|
```typescript
|
||||||
|
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
|
||||||
|
export type ItemTier = "fixed" | "variable" | "one_off"
|
||||||
|
export interface Profile { id: string; display_name: string | null; locale: string; currency: string; setup_completed: boolean; ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/data/presets.ts:
|
||||||
|
```typescript
|
||||||
|
export interface PresetItem {
|
||||||
|
slug: string
|
||||||
|
type: CategoryType
|
||||||
|
defaultAmount: number
|
||||||
|
item_tier: "fixed" | "variable"
|
||||||
|
}
|
||||||
|
export const PRESETS: PresetItem[] = [ /* 19 items: 4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment */ ]
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useAuth.ts:
|
||||||
|
```typescript
|
||||||
|
// Returns { user, profile, loading, signIn, signOut, signUp }
|
||||||
|
// profile.currency = "EUR" | "USD" | "CHF" etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useCategories.ts:
|
||||||
|
```typescript
|
||||||
|
export function useCategories(): { categories: Category[]; loading: boolean; create: UseMutationResult; ... }
|
||||||
|
// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useTemplate.ts:
|
||||||
|
```typescript
|
||||||
|
export function useTemplate(): { template: Template | null; items: TemplateItem[]; loading: boolean; createItem: UseMutationResult; ... }
|
||||||
|
// createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Install shadcn checkbox + create useWizardState hook + add i18n keys</name>
|
||||||
|
<read_first>
|
||||||
|
- src/i18n/en.json
|
||||||
|
- src/i18n/de.json
|
||||||
|
- src/data/presets.ts
|
||||||
|
- src/hooks/useAuth.ts
|
||||||
|
</read_first>
|
||||||
|
<files>src/hooks/useWizardState.ts, src/i18n/en.json, src/i18n/de.json</files>
|
||||||
|
<action>
|
||||||
|
1. Install shadcn checkbox:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add checkbox
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `src/hooks/useWizardState.ts`:
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { PRESETS } from "@/data/presets"
|
||||||
|
|
||||||
|
export interface WizardState {
|
||||||
|
currentStep: 1 | 2 | 3
|
||||||
|
income: number
|
||||||
|
selectedItems: Record<string, { checked: boolean; amount: number }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}`
|
||||||
|
|
||||||
|
function getDefaultState(): WizardState {
|
||||||
|
const selectedItems: Record<string, { checked: boolean; amount: number }> = {}
|
||||||
|
for (const preset of PRESETS) {
|
||||||
|
selectedItems[preset.slug] = {
|
||||||
|
checked: preset.type === "bill" || preset.type === "variable_expense",
|
||||||
|
amount: preset.defaultAmount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { currentStep: 1, income: 3000, selectedItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWizardState(userId: string) {
|
||||||
|
const [state, setState] = useState<WizardState>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY(userId))
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved)
|
||||||
|
// Validate shape
|
||||||
|
if (parsed && typeof parsed.currentStep === "number" && typeof parsed.income === "number" && parsed.selectedItems) {
|
||||||
|
return parsed as WizardState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore corrupt data */ }
|
||||||
|
return getDefaultState()
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state))
|
||||||
|
}, [state, userId])
|
||||||
|
|
||||||
|
const setStep = (step: 1 | 2 | 3) => setState(s => ({ ...s, currentStep: step }))
|
||||||
|
const setIncome = (income: number) => setState(s => ({ ...s, income }))
|
||||||
|
const toggleItem = (slug: string) => setState(s => ({
|
||||||
|
...s,
|
||||||
|
selectedItems: {
|
||||||
|
...s.selectedItems,
|
||||||
|
[slug]: { ...s.selectedItems[slug], checked: !s.selectedItems[slug].checked },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const setItemAmount = (slug: string, amount: number) => setState(s => ({
|
||||||
|
...s,
|
||||||
|
selectedItems: {
|
||||||
|
...s.selectedItems,
|
||||||
|
[slug]: { ...s.selectedItems[slug], amount },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const clearState = () => {
|
||||||
|
localStorage.removeItem(STORAGE_KEY(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state, setStep, setIncome, toggleItem, setItemAmount, clearState }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add i18n keys to `src/i18n/en.json` — add a `"setup"` object at the top level:
|
||||||
|
```json
|
||||||
|
"setup": {
|
||||||
|
"title": "Set up your budget",
|
||||||
|
"step1": {
|
||||||
|
"title": "Monthly Income",
|
||||||
|
"description": "How much do you earn each month?",
|
||||||
|
"incomeLabel": "Monthly net income",
|
||||||
|
"helper": "Enter your total monthly take-home pay",
|
||||||
|
"validation": "Please enter a positive income amount"
|
||||||
|
},
|
||||||
|
"step2": {
|
||||||
|
"title": "Recurring Items",
|
||||||
|
"description": "Select your regular monthly expenses",
|
||||||
|
"remaining": "Remaining to allocate"
|
||||||
|
},
|
||||||
|
"step3": {
|
||||||
|
"title": "Review",
|
||||||
|
"description": "Confirm your budget template",
|
||||||
|
"incomeLabel": "Monthly income",
|
||||||
|
"totalLabel": "Total expenses",
|
||||||
|
"remainingLabel": "Remaining",
|
||||||
|
"empty": "No items selected. You can add items to your template later."
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"1": "Income",
|
||||||
|
"2": "Items",
|
||||||
|
"3": "Review"
|
||||||
|
},
|
||||||
|
"next": "Next Step",
|
||||||
|
"back": "Go Back",
|
||||||
|
"skip": "Skip Step",
|
||||||
|
"skipSetup": "Skip setup",
|
||||||
|
"complete": "Complete Setup",
|
||||||
|
"toast": {
|
||||||
|
"success": "Template created! Your first budget will appear automatically.",
|
||||||
|
"error": "Could not save your template. Please try again.",
|
||||||
|
"partialError": "Some items could not be saved. Check your template page."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add equivalent German keys to `src/i18n/de.json`:
|
||||||
|
```json
|
||||||
|
"setup": {
|
||||||
|
"title": "Budget einrichten",
|
||||||
|
"step1": {
|
||||||
|
"title": "Monatliches Einkommen",
|
||||||
|
"description": "Wie viel verdienst du pro Monat?",
|
||||||
|
"incomeLabel": "Monatliches Nettoeinkommen",
|
||||||
|
"helper": "Gib dein monatliches Nettoeinkommen ein",
|
||||||
|
"validation": "Bitte gib einen positiven Betrag ein"
|
||||||
|
},
|
||||||
|
"step2": {
|
||||||
|
"title": "Wiederkehrende Posten",
|
||||||
|
"description": "Wahle deine regelmaigen monatlichen Ausgaben",
|
||||||
|
"remaining": "Verbleibend zu verteilen"
|
||||||
|
},
|
||||||
|
"step3": {
|
||||||
|
"title": "Ubersicht",
|
||||||
|
"description": "Bestatige deine Budgetvorlage",
|
||||||
|
"incomeLabel": "Monatliches Einkommen",
|
||||||
|
"totalLabel": "Gesamtausgaben",
|
||||||
|
"remainingLabel": "Verbleibend",
|
||||||
|
"empty": "Keine Posten ausgewahlt. Du kannst spater Posten zu deiner Vorlage hinzufugen."
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"1": "Einkommen",
|
||||||
|
"2": "Posten",
|
||||||
|
"3": "Ubersicht"
|
||||||
|
},
|
||||||
|
"next": "Nachster Schritt",
|
||||||
|
"back": "Zuruck",
|
||||||
|
"skip": "Schritt uberspringen",
|
||||||
|
"skipSetup": "Einrichtung uberspringen",
|
||||||
|
"complete": "Einrichtung abschlieen",
|
||||||
|
"toast": {
|
||||||
|
"success": "Vorlage erstellt! Dein erstes Budget wird automatisch erscheinen.",
|
||||||
|
"error": "Vorlage konnte nicht gespeichert werden. Bitte versuche es erneut.",
|
||||||
|
"partialError": "Einige Posten konnten nicht gespeichert werden. Prufe deine Vorlagenseite."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -q "useWizardState" src/hooks/useWizardState.ts && grep -q '"setup"' src/i18n/en.json && grep -q '"setup"' src/i18n/de.json && ls src/components/ui/checkbox.tsx</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- src/hooks/useWizardState.ts contains `export function useWizardState(userId: string)`
|
||||||
|
- src/hooks/useWizardState.ts contains `setup-wizard-${userId}`
|
||||||
|
- src/hooks/useWizardState.ts contains `getDefaultState`
|
||||||
|
- src/i18n/en.json contains `"setup.title"` nested under `"setup": { "title":`
|
||||||
|
- src/i18n/en.json contains `"setup.toast.success"` with value "Template created!"
|
||||||
|
- src/i18n/de.json contains `"setup": { "title": "Budget einrichten"`
|
||||||
|
- src/components/ui/checkbox.tsx exists (shadcn installed)
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>useWizardState hook created with localStorage sync, all setup i18n keys in EN+DE, shadcn checkbox component installed</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create all wizard UI components and SetupPage</name>
|
||||||
|
<read_first>
|
||||||
|
- src/pages/LoginPage.tsx
|
||||||
|
- src/components/dashboard/SummaryStrip.tsx
|
||||||
|
- src/pages/SettingsPage.tsx
|
||||||
|
- src/data/presets.ts
|
||||||
|
- src/hooks/useWizardState.ts
|
||||||
|
- src/components/ui/checkbox.tsx
|
||||||
|
</read_first>
|
||||||
|
<files>src/components/setup/WizardStepper.tsx, src/components/setup/IncomeStep.tsx, src/components/setup/AllocationBar.tsx, src/components/setup/CategoryGroupHeader.tsx, src/components/setup/PresetItemRow.tsx, src/components/setup/RecurringItemsStep.tsx, src/pages/SetupPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
Create the following components in `src/components/setup/`:
|
||||||
|
|
||||||
|
**1. WizardStepper.tsx:**
|
||||||
|
- Props: `currentStep: 1|2|3`, `onStepClick: (step: 1|2|3) => void`
|
||||||
|
- Renders 3 circles in a row connected by lines
|
||||||
|
- Active step: `bg-primary text-primary-foreground` with number (14px/600)
|
||||||
|
- Completed step (step < currentStep): `bg-primary text-primary-foreground` with lucide `Check` icon (16px)
|
||||||
|
- Upcoming step (step > currentStep): `bg-muted text-muted-foreground border border-border`
|
||||||
|
- Connector line between: `h-px w-16` — completed segment `bg-primary`, upcoming `bg-border`
|
||||||
|
- Step labels below circles: text-xs font-semibold, active `text-foreground`, others `text-muted-foreground`
|
||||||
|
- Labels from i18n: `t("setup.steps.1")`, `t("setup.steps.2")`, `t("setup.steps.3")`
|
||||||
|
- Clicking a completed step or current step calls onStepClick. Future steps: no action, cursor-default.
|
||||||
|
- Wrap in `role="navigation" aria-label="Setup progress"`
|
||||||
|
- Circle dimensions: `w-8 h-8` (32px)
|
||||||
|
- Overall layout: `flex items-center justify-center gap-0` with step groups `flex flex-col items-center gap-1` separated by connector lines
|
||||||
|
- Margin below: handled by parent (mb-6)
|
||||||
|
|
||||||
|
**2. IncomeStep.tsx:**
|
||||||
|
- Props: `income: number`, `onIncomeChange: (val: number) => void`, `currency: string`, `error: string | null`
|
||||||
|
- Renders card content for step 1
|
||||||
|
- Label: `t("setup.step1.incomeLabel")` (14px/600)
|
||||||
|
- Input row: `flex items-center gap-2`
|
||||||
|
- Input: `type="number"`, `className="w-full text-right text-lg"`, value={income}, onChange converts to number
|
||||||
|
- Currency suffix: `<span className="text-muted-foreground text-sm">{currency}</span>`
|
||||||
|
- Helper text: `t("setup.step1.helper")` in `text-sm text-muted-foreground`
|
||||||
|
- If `error` is truthy: show `<p className="text-sm text-destructive">{error}</p>` below input
|
||||||
|
- `aria-describedby` on input pointing to helper and error elements
|
||||||
|
|
||||||
|
**3. AllocationBar.tsx:**
|
||||||
|
- Props: `remaining: number`, `currency: string`
|
||||||
|
- Layout: `sticky top-0 z-10 flex justify-between items-center py-3 px-4 bg-muted border-b border-border rounded-none`
|
||||||
|
- Left: `<span className="text-sm font-semibold">{t("setup.step2.remaining")}</span>`
|
||||||
|
- Right: formatted amount using `Intl.NumberFormat` with currency
|
||||||
|
- `remaining >= 0`: `className="text-base font-semibold text-on-budget"`
|
||||||
|
- `remaining < 0`: `className="text-base font-semibold text-destructive"`
|
||||||
|
- Add `aria-live="polite"` on the amount element
|
||||||
|
|
||||||
|
**4. CategoryGroupHeader.tsx:**
|
||||||
|
- Props: `type: CategoryType`, `label: string`, `count: number`
|
||||||
|
- Layout: `flex items-center gap-2 pt-4 pb-2`
|
||||||
|
- Colored dot: `w-2.5 h-2.5 rounded-full` with inline style `backgroundColor: var(--color-${type.replace('_', '-')})`
|
||||||
|
- Map types to CSS vars: income -> `--color-income`, bill -> `--color-bill`, variable_expense -> `--color-variable-expense`, debt -> `--color-debt`, saving -> `--color-saving`, investment -> `--color-investment`
|
||||||
|
- Label: `<span className="text-sm font-semibold">{label}</span>`
|
||||||
|
- Count: `<span className="text-xs text-muted-foreground">({count} items)</span>`
|
||||||
|
- Below: `<Separator />` from shadcn
|
||||||
|
|
||||||
|
**5. PresetItemRow.tsx:**
|
||||||
|
- Props: `slug: string`, `type: CategoryType`, `checked: boolean`, `amount: number`, `onToggle: () => void`, `onAmountChange: (val: number) => void`
|
||||||
|
- Layout: `flex items-center gap-3 py-2.5 px-1`
|
||||||
|
- Checkbox: shadcn `<Checkbox checked={checked} onCheckedChange={onToggle} />`
|
||||||
|
- Item name: `<span className="flex-1 text-sm">{t(\`presets.${type}.${slug}\`)}</span>`
|
||||||
|
- Category badge: `<Badge variant="outline" className="text-xs" style={{ borderLeftWidth: '3px', borderLeftColor: \`var(--color-${type.replace('_', '-')})\` }}>{t(\`categories.types.${type}\`)}</Badge>`
|
||||||
|
- Amount input: `<Input type="number" className="w-24 text-right" value={amount} onChange={...} disabled={!checked} />`
|
||||||
|
- When disabled: add `bg-muted text-muted-foreground opacity-50` classes
|
||||||
|
|
||||||
|
**6. RecurringItemsStep.tsx:**
|
||||||
|
- Props: `selectedItems: Record<string, { checked: boolean; amount: number }>`, `income: number`, `currency: string`, `onToggle: (slug: string) => void`, `onAmountChange: (slug: string, amount: number) => void`
|
||||||
|
- Groups PRESETS by type using: `Object.groupBy(PRESETS, (p) => p.type)` — if Object.groupBy not available, use manual reduce
|
||||||
|
- Order groups: `["income", "bill", "variable_expense", "debt", "saving", "investment"]`
|
||||||
|
- Renders AllocationBar at top (sticky)
|
||||||
|
- For each group: CategoryGroupHeader + list of PresetItemRow
|
||||||
|
- Wrap each group in `<fieldset>` with `<legend className="sr-only">{groupLabel}</legend>`
|
||||||
|
- Compute remaining: `income - sum of all checked items' amounts`
|
||||||
|
|
||||||
|
**7. SetupPage.tsx** (page orchestrator):
|
||||||
|
- Imports useAuth, useWizardState, useTranslation, WizardStepper, IncomeStep, RecurringItemsStep
|
||||||
|
- Gets userId from `useAuth()` -> `user.id`
|
||||||
|
- Gets currency from profile: `profile?.currency ?? "EUR"`
|
||||||
|
- Uses `useWizardState(userId)` for state management
|
||||||
|
- Layout: `<div className="flex min-h-screen items-center justify-center bg-background p-4"><div className="w-full max-w-2xl">`
|
||||||
|
- Renders WizardStepper above the card
|
||||||
|
- Card: `<Card className="w-full border border-border shadow-sm">`
|
||||||
|
- CardHeader: step title (heading 20px/600) + step description (body 14px/400 text-muted-foreground)
|
||||||
|
- CardContent: renders active step component based on `state.currentStep`
|
||||||
|
- Bottom nav: `flex justify-between items-center pt-4 border-t border-border`
|
||||||
|
- Left side: Go Back button (variant="ghost", hidden on step 1) + Skip Step button (variant="ghost" text-muted-foreground)
|
||||||
|
- Right side: Next Step button (variant="default")
|
||||||
|
- Step 1 Next: validates income > 0, shows error if invalid, otherwise advances to step 2
|
||||||
|
- Step 2 Next: no validation, advances to step 3
|
||||||
|
- Step 3: shows placeholder text "Review step coming in next plan" (will be replaced in Plan 02)
|
||||||
|
- Below card: "Skip setup" link: `<button className="mt-4 text-sm text-muted-foreground underline block mx-auto">{t("setup.skipSetup")}</button>`
|
||||||
|
- Skip step on step 1: advance to step 2. Skip step on step 2: advance to step 3.
|
||||||
|
- Clicking completed stepper steps calls `setStep(step)`
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>ls src/components/setup/WizardStepper.tsx src/components/setup/IncomeStep.tsx src/components/setup/AllocationBar.tsx src/components/setup/CategoryGroupHeader.tsx src/components/setup/PresetItemRow.tsx src/components/setup/RecurringItemsStep.tsx src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- src/pages/SetupPage.tsx contains `useWizardState` and `max-w-2xl`
|
||||||
|
- src/pages/SetupPage.tsx contains `currentStep` conditional rendering
|
||||||
|
- src/components/setup/WizardStepper.tsx contains `role="navigation"` and `aria-label`
|
||||||
|
- src/components/setup/WizardStepper.tsx contains `w-8 h-8`
|
||||||
|
- src/components/setup/IncomeStep.tsx contains `type="number"` and `text-lg`
|
||||||
|
- src/components/setup/AllocationBar.tsx contains `aria-live="polite"` and `sticky top-0`
|
||||||
|
- src/components/setup/CategoryGroupHeader.tsx contains `w-2.5 h-2.5 rounded-full`
|
||||||
|
- src/components/setup/PresetItemRow.tsx contains `Checkbox` import and `w-24`
|
||||||
|
- src/components/setup/RecurringItemsStep.tsx imports `PRESETS` from `@/data/presets`
|
||||||
|
- TypeScript compiles without errors (`npx tsc --noEmit` exits 0)
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>All 7 wizard UI components render correctly, TypeScript compiles without errors, wizard navigates between steps 1 and 2 with working state persistence</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| localStorage -> component | Persisted wizard data read back into React state |
|
||||||
|
| client -> Supabase (future Plan 02) | Category/template item creation on completion |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-07-01 | Tampering | useWizardState localStorage read | mitigate | Validate JSON shape on load: check currentStep is 1-3, income is number, selectedItems is object with expected structure. Fall back to defaults on invalid data. |
|
||||||
|
| T-07-02 | Information Disclosure | localStorage key | accept | Key includes userId, preventing cross-user data leakage. Data is non-sensitive (income amount, item selections). |
|
||||||
|
| T-07-03 | Spoofing | Preset data | accept | PRESETS are hardcoded source constants, not user input. i18n keys resolve from bundled JSON, no injection vector. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` passes (no type errors)
|
||||||
|
- All 7 component files exist in src/components/setup/ and src/pages/
|
||||||
|
- useWizardState stores and retrieves state from localStorage
|
||||||
|
- i18n keys resolve for both "en" and "de" locales
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- SetupPage renders with WizardStepper showing 3 steps
|
||||||
|
- Step 1 shows income input pre-filled with 3000 and currency suffix
|
||||||
|
- Step 2 shows all 19 PRESETS grouped by type with checkboxes and editable amounts
|
||||||
|
- Bills (4) and variable_expense (5) items are checked by default
|
||||||
|
- AllocationBar shows remaining = 3000 - sum(checked) and turns red when negative
|
||||||
|
- Navigating between steps preserves state
|
||||||
|
- Refreshing the page restores wizard at correct step (localStorage)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/07-setup-wizard/07-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
392
.planning/phases/07-setup-wizard/07-02-PLAN.md
Normal file
392
.planning/phases/07-setup-wizard/07-02-PLAN.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
---
|
||||||
|
phase: 07-setup-wizard
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [07-01]
|
||||||
|
files_modified:
|
||||||
|
- src/components/setup/ReviewStep.tsx
|
||||||
|
- src/pages/SetupPage.tsx
|
||||||
|
- src/App.tsx
|
||||||
|
- src/pages/DashboardPage.tsx
|
||||||
|
autonomous: true
|
||||||
|
requirements: [SETUP-01, SETUP-03, SETUP-05]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "ReviewStep shows read-only summary of income + selected items grouped by type + totals"
|
||||||
|
- "/setup route is protected and renders SetupPage outside AppLayout"
|
||||||
|
- "DashboardPage redirects first-run users to /setup via useFirstRunState"
|
||||||
|
- "Completing wizard creates categories then template items and redirects to dashboard"
|
||||||
|
- "Skip setup clears localStorage, marks setup_completed=true, redirects to dashboard"
|
||||||
|
- "Complete button is disabled during API calls to prevent double-submit"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/components/setup/ReviewStep.tsx"
|
||||||
|
provides: "Read-only summary of wizard selections"
|
||||||
|
exports: ["ReviewStep"]
|
||||||
|
- path: "src/App.tsx"
|
||||||
|
provides: "/setup route registration"
|
||||||
|
contains: "path=\"/setup\""
|
||||||
|
- path: "src/pages/DashboardPage.tsx"
|
||||||
|
provides: "First-run redirect to /setup"
|
||||||
|
contains: "useFirstRunState"
|
||||||
|
key_links:
|
||||||
|
- from: "src/pages/DashboardPage.tsx"
|
||||||
|
to: "src/hooks/useFirstRunState.ts"
|
||||||
|
via: "useFirstRunState() -> isFirstRun -> Navigate to /setup"
|
||||||
|
pattern: "isFirstRun.*Navigate.*setup"
|
||||||
|
- from: "src/pages/SetupPage.tsx"
|
||||||
|
to: "src/hooks/useCategories.ts"
|
||||||
|
via: "create.mutateAsync on wizard completion"
|
||||||
|
pattern: "create\\.mutateAsync"
|
||||||
|
- from: "src/pages/SetupPage.tsx"
|
||||||
|
to: "src/hooks/useTemplate.ts"
|
||||||
|
via: "createItem.mutateAsync on wizard completion"
|
||||||
|
pattern: "createItem\\.mutateAsync"
|
||||||
|
- from: "src/App.tsx"
|
||||||
|
to: "src/pages/SetupPage.tsx"
|
||||||
|
via: "Route path=/setup element=SetupPage"
|
||||||
|
pattern: "path=.*/setup"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Add the ReviewStep component, wire wizard completion logic (creating categories + template items), add skip functionality, register the /setup route, and add first-run redirect in DashboardPage.
|
||||||
|
|
||||||
|
Purpose: Completes the wizard end-to-end — a new user is redirected to /setup, can navigate all 3 steps, and either completes (creating template data) or skips.
|
||||||
|
Output: Fully functional setup wizard accessible via /setup with working completion and skip flows.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-CONTEXT.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-RESEARCH.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-PATTERNS.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-UI-SPEC.md
|
||||||
|
@.planning/phases/07-setup-wizard/07-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
From src/hooks/useWizardState.ts (created in Plan 01):
|
||||||
|
```typescript
|
||||||
|
export interface WizardState {
|
||||||
|
currentStep: 1 | 2 | 3
|
||||||
|
income: number
|
||||||
|
selectedItems: Record<string, { checked: boolean; amount: number }>
|
||||||
|
}
|
||||||
|
export function useWizardState(userId: string): {
|
||||||
|
state: WizardState
|
||||||
|
setStep: (step: 1 | 2 | 3) => void
|
||||||
|
setIncome: (income: number) => void
|
||||||
|
toggleItem: (slug: string) => void
|
||||||
|
setItemAmount: (slug: string, amount: number) => void
|
||||||
|
clearState: () => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useCategories.ts:
|
||||||
|
```typescript
|
||||||
|
// create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category { id, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useTemplate.ts:
|
||||||
|
```typescript
|
||||||
|
// createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem
|
||||||
|
// Template auto-creates on first useTemplate() query
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/hooks/useFirstRunState.ts:
|
||||||
|
```typescript
|
||||||
|
export function useFirstRunState(): { isFirstRun: boolean; loading: boolean }
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/data/presets.ts:
|
||||||
|
```typescript
|
||||||
|
export const PRESETS: PresetItem[] // 19 items with slug, type, defaultAmount, item_tier
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create ReviewStep + wire completion/skip logic into SetupPage</name>
|
||||||
|
<read_first>
|
||||||
|
- src/pages/SetupPage.tsx
|
||||||
|
- src/hooks/useCategories.ts
|
||||||
|
- src/hooks/useTemplate.ts
|
||||||
|
- src/hooks/useWizardState.ts
|
||||||
|
- src/data/presets.ts
|
||||||
|
- src/lib/supabase.ts
|
||||||
|
</read_first>
|
||||||
|
<files>src/components/setup/ReviewStep.tsx, src/pages/SetupPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
**1. Create `src/components/setup/ReviewStep.tsx`:**
|
||||||
|
|
||||||
|
Props: `income: number`, `selectedItems: Record<string, { checked: boolean; amount: number }>`, `currency: string`
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
- Income row: `flex justify-between py-2` — left: `t("setup.step3.incomeLabel")` (14px/600), right: formatted income (14px/600)
|
||||||
|
- `<Separator />`
|
||||||
|
- Group checked items by type (same order: income, bill, variable_expense, debt, saving, investment). Only show groups that have at least one checked item.
|
||||||
|
- For each group: render CategoryGroupHeader (import from `./CategoryGroupHeader`), then for each checked item in group: `<div className="flex justify-between py-1.5 px-1"><span className="text-sm">{t(\`presets.${type}.${slug}\`)}</span><span className="text-sm text-right">{formattedAmount}</span></div>`
|
||||||
|
- `<Separator />`
|
||||||
|
- Totals: `space-y-1 pt-2`
|
||||||
|
- Total expenses row: `flex justify-between` with label `t("setup.step3.totalLabel")` (14px/600) and sum of checked amounts
|
||||||
|
- Remaining row: `flex justify-between` with label `t("setup.step3.remainingLabel")` (16px/600), value colored `text-on-budget` if >= 0, `text-destructive` if < 0
|
||||||
|
- If no items checked: show `<p className="text-sm text-muted-foreground py-4">{t("setup.step3.empty")}</p>` instead of groups
|
||||||
|
|
||||||
|
Use `Intl.NumberFormat` with `style: "currency"` and the `currency` prop for all amount formatting.
|
||||||
|
|
||||||
|
Import PRESETS from `@/data/presets` to map slugs to types for grouping.
|
||||||
|
|
||||||
|
**2. Update `src/pages/SetupPage.tsx` to add:**
|
||||||
|
|
||||||
|
a) Import ReviewStep, useCategories, useTemplate, useQueryClient, toast (from sonner), supabase (from @/lib/supabase), Navigate (from react-router-dom), useNavigate
|
||||||
|
|
||||||
|
b) Add `completing` state: `const [completing, setCompleting] = useState(false)`
|
||||||
|
|
||||||
|
c) Replace the step 3 placeholder with `<ReviewStep income={state.income} selectedItems={state.selectedItems} currency={currency} />`
|
||||||
|
|
||||||
|
d) Change the right-side button on step 3 from "Next Step" to "Complete Setup" (`t("setup.complete")`), disabled when `completing`
|
||||||
|
|
||||||
|
e) Add completion handler `handleComplete`:
|
||||||
|
```typescript
|
||||||
|
async function handleComplete() {
|
||||||
|
setCompleting(true)
|
||||||
|
try {
|
||||||
|
const queryClient = useQueryClient() // already declared at top
|
||||||
|
const checkedItems = PRESETS.filter(p => state.selectedItems[p.slug]?.checked)
|
||||||
|
|
||||||
|
// 1. Determine unique category types needed
|
||||||
|
const typesNeeded = [...new Set(checkedItems.map(i => i.type))]
|
||||||
|
|
||||||
|
// 2. Create one category per type (use i18n label as name)
|
||||||
|
const categoryMap: Record<string, string> = {}
|
||||||
|
for (const type of typesNeeded) {
|
||||||
|
try {
|
||||||
|
const cat = await create.mutateAsync({ name: t(`categories.types.${type}`), type })
|
||||||
|
categoryMap[type] = cat.id
|
||||||
|
} catch (e: any) {
|
||||||
|
// Unique constraint violation (23505) = category already exists, fetch it
|
||||||
|
if (e?.code === "23505" || e?.message?.includes("duplicate")) {
|
||||||
|
const existing = categories.find(c => c.type === type)
|
||||||
|
if (existing) categoryMap[type] = existing.id
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. If categories were fetched from cache but not found, refetch
|
||||||
|
if (Object.keys(categoryMap).length < typesNeeded.length) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["categories"] })
|
||||||
|
// Remaining types need fresh data — handled by the existing entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create template items for each checked preset
|
||||||
|
let partialFailure = false
|
||||||
|
for (const item of checkedItems) {
|
||||||
|
try {
|
||||||
|
await createItem.mutateAsync({
|
||||||
|
category_id: categoryMap[item.type],
|
||||||
|
item_tier: item.item_tier,
|
||||||
|
budgeted_amount: state.selectedItems[item.slug].amount,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
partialFailure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Mark setup complete
|
||||||
|
await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id)
|
||||||
|
|
||||||
|
// 6. Clear wizard state
|
||||||
|
clearState()
|
||||||
|
|
||||||
|
// 7. Invalidate queries to prevent redirect loop
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["categories"] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["template-items"] })
|
||||||
|
|
||||||
|
// 8. Toast and redirect
|
||||||
|
if (partialFailure) {
|
||||||
|
toast.error(t("setup.toast.partialError"))
|
||||||
|
} else {
|
||||||
|
toast.success(t("setup.toast.success"))
|
||||||
|
}
|
||||||
|
navigate("/", { replace: true })
|
||||||
|
} catch {
|
||||||
|
toast.error(t("setup.toast.error"))
|
||||||
|
} finally {
|
||||||
|
setCompleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
f) Add skip handler `handleSkipSetup`:
|
||||||
|
```typescript
|
||||||
|
async function handleSkipSetup() {
|
||||||
|
clearState()
|
||||||
|
await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id)
|
||||||
|
navigate("/", { replace: true })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
g) Wire "Skip setup" button below card to call `handleSkipSetup`
|
||||||
|
|
||||||
|
h) Wire "Skip Step" on step 3 to also call `handleSkipSetup` (per UI-SPEC: "On step 3 skip: same as Skip setup")
|
||||||
|
|
||||||
|
i) Disable Go Back, Skip Step, and Complete Setup buttons when `completing` is true
|
||||||
|
|
||||||
|
j) Add `useCategories()` call at component top level to get `create` mutation and `categories` array. Add `useTemplate()` to ensure template row exists before completion (Pitfall 5).
|
||||||
|
|
||||||
|
k) Get `useQueryClient()` at component top level (not inside handler).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -q "ReviewStep" src/components/setup/ReviewStep.tsx && grep -q "handleComplete" src/pages/SetupPage.tsx && grep -q "setup_completed.*true" src/pages/SetupPage.tsx && grep -q "clearState" src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- src/components/setup/ReviewStep.tsx contains `t("setup.step3.incomeLabel")` and `Intl.NumberFormat`
|
||||||
|
- src/components/setup/ReviewStep.tsx contains `text-on-budget` and `text-destructive` conditional
|
||||||
|
- src/pages/SetupPage.tsx contains `async function handleComplete`
|
||||||
|
- src/pages/SetupPage.tsx contains `create.mutateAsync` (category creation)
|
||||||
|
- src/pages/SetupPage.tsx contains `createItem.mutateAsync` (template item creation)
|
||||||
|
- src/pages/SetupPage.tsx contains `setup_completed: true` (profile update)
|
||||||
|
- src/pages/SetupPage.tsx contains `handleSkipSetup` function
|
||||||
|
- src/pages/SetupPage.tsx contains `setCompleting(true)` (double-click prevention)
|
||||||
|
- src/pages/SetupPage.tsx contains `toast.success` and `toast.error`
|
||||||
|
- src/pages/SetupPage.tsx contains `invalidateQueries.*categories` (redirect loop prevention)
|
||||||
|
- TypeScript compiles without errors
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>ReviewStep renders read-only grouped summary. Completion creates categories + template items, handles duplicates, clears state, marks setup_completed, toasts, and redirects. Skip exits without creating data.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Register /setup route in App.tsx + add first-run redirect in DashboardPage</name>
|
||||||
|
<read_first>
|
||||||
|
- src/App.tsx
|
||||||
|
- src/pages/DashboardPage.tsx
|
||||||
|
- src/hooks/useFirstRunState.ts
|
||||||
|
</read_first>
|
||||||
|
<files>src/App.tsx, src/pages/DashboardPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
**1. Update `src/App.tsx`:**
|
||||||
|
|
||||||
|
Add import at top:
|
||||||
|
```typescript
|
||||||
|
import SetupPage from "@/pages/SetupPage"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the /setup route AFTER the public routes (login, register) and BEFORE the AppLayout route. It must be inside a ProtectedRoute but NOT inside AppLayout:
|
||||||
|
```typescript
|
||||||
|
<Route
|
||||||
|
path="/setup"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<SetupPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert this between the `</Route>` closing the register route (around line 46) and the `<Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>` line (around line 47).
|
||||||
|
|
||||||
|
**2. Update `src/pages/DashboardPage.tsx`:**
|
||||||
|
|
||||||
|
Add imports at top (after existing imports):
|
||||||
|
```typescript
|
||||||
|
import { useFirstRunState } from "@/hooks/useFirstRunState"
|
||||||
|
import { Navigate } from "react-router-dom"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `Navigate` may already be imported — check before adding a duplicate.
|
||||||
|
|
||||||
|
Inside the DashboardPage component function, add early returns BEFORE any existing logic (before the existing useState/useMemo calls):
|
||||||
|
```typescript
|
||||||
|
const { isFirstRun, loading: firstRunLoading } = useFirstRunState()
|
||||||
|
|
||||||
|
if (firstRunLoading) return <DashboardSkeleton />
|
||||||
|
if (isFirstRun) return <Navigate to="/setup" replace />
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses the existing `DashboardSkeleton` component (already imported) as the loading fallback.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -q 'path="/setup"' src/App.tsx && grep -q "SetupPage" src/App.tsx && grep -q "useFirstRunState" src/pages/DashboardPage.tsx && grep -q 'Navigate to="/setup"' src/pages/DashboardPage.tsx && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- src/App.tsx contains `import SetupPage from "@/pages/SetupPage"`
|
||||||
|
- src/App.tsx contains `path="/setup"` within a `<ProtectedRoute>` wrapper
|
||||||
|
- src/App.tsx has /setup route BEFORE the AppLayout route (not nested inside it)
|
||||||
|
- src/pages/DashboardPage.tsx contains `import { useFirstRunState } from "@/hooks/useFirstRunState"`
|
||||||
|
- src/pages/DashboardPage.tsx contains `const { isFirstRun, loading: firstRunLoading } = useFirstRunState()`
|
||||||
|
- src/pages/DashboardPage.tsx contains `if (isFirstRun) return <Navigate to="/setup" replace />`
|
||||||
|
- src/pages/DashboardPage.tsx contains `if (firstRunLoading) return <DashboardSkeleton />`
|
||||||
|
- TypeScript compiles without errors
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>/setup route registered as protected standalone page. First-run users are redirected from dashboard to /setup. Existing users with categories/template data see the normal dashboard.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Complete 3-step setup wizard with income entry, recurring items checklist (19 presets), review step, completion logic (category + template creation), skip functionality, and first-run redirect from dashboard.</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. Create a new test user (or clear an existing user's categories and template items in Supabase)
|
||||||
|
2. Log in with the test user — should be redirected to `/setup`
|
||||||
|
3. Step 1: Verify income input shows 3000 pre-filled with EUR suffix. Try Next with empty/0 value (should show validation error). Enter 4000, click Next.
|
||||||
|
4. Step 2: Verify Bills (4) and Variable Expenses (5) are pre-checked. Verify allocation bar shows "Remaining to allocate: X" (4000 - sum of checked). Uncheck an item — verify amount updates. Check an item — verify it enables the amount input.
|
||||||
|
5. Step 3: Verify read-only summary shows income, grouped checked items, totals, and remaining.
|
||||||
|
6. Click "Complete Setup" — verify toast appears, redirected to dashboard, template page shows created items.
|
||||||
|
7. Refresh dashboard — should NOT redirect back to /setup (setup_completed=true, and categories exist).
|
||||||
|
8. Test "Skip setup" link — create another fresh user, click "Skip setup" from step 1. Verify redirect to dashboard with no data created.
|
||||||
|
9. Test localStorage persistence: start wizard, enter income, advance to step 2, refresh page — verify wizard resumes at step 2 with entered income.
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" or describe any issues found</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| client -> Supabase | Category and template item inserts on completion |
|
||||||
|
| localStorage -> component | Wizard state restoration (validated in Plan 01) |
|
||||||
|
| React Query cache -> redirect logic | isFirstRun depends on cache freshness |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-07-04 | Denial of Service | handleComplete double-click | mitigate | `completing` state disables button immediately on click. DB unique constraints on categories prevent duplicate rows. |
|
||||||
|
| T-07-05 | Tampering | Supabase profile update (setup_completed) | accept | RLS policy restricts updates to own profile row. Client can only set own setup_completed. |
|
||||||
|
| T-07-06 | Elevation of Privilege | /setup route access | mitigate | Route wrapped in ProtectedRoute — unauthenticated users redirected to /login. |
|
||||||
|
| T-07-07 | Denial of Service | Redirect loop (dashboard <-> /setup) | mitigate | After completion, invalidate ["categories"] and ["template-items"] queries before redirect. useFirstRunState will read fresh data showing isFirstRun=false. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` passes
|
||||||
|
- Navigating to /setup while unauthenticated redirects to /login
|
||||||
|
- A user with zero categories sees /setup after login
|
||||||
|
- Completing wizard creates categories + template items in database
|
||||||
|
- Skipping wizard sets setup_completed=true without creating data
|
||||||
|
- Refreshing mid-wizard restores state from localStorage
|
||||||
|
- No redirect loop after completion
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- New user flow: login -> auto-redirect to /setup -> 3 steps -> complete -> dashboard with template
|
||||||
|
- Skip flow: /setup -> skip -> dashboard (no data created, setup_completed=true)
|
||||||
|
- Existing user flow: login -> dashboard (no redirect to /setup)
|
||||||
|
- No duplicate categories on repeated completion attempts
|
||||||
|
- No infinite redirect loop between dashboard and /setup
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/07-setup-wizard/07-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user