`, `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).