docs: complete project research

This commit is contained in:
2026-04-02 15:30:56 +02:00
parent 8fdab4b796
commit 0b72f1c305
5 changed files with 1204 additions and 1010 deletions

View File

@@ -1,452 +1,545 @@
# Architecture Research
**Domain:** Personal finance dashboard UI — React SPA overhaul
**Researched:** 2026-03-16
**Confidence:** HIGH (existing codebase is fully inspected; patterns are grounded in Radix/shadcn/Recharts official docs)
**Domain:** Personal budget dashboard — v2.0 UX simplification with wizard setup, auto-budget creation, inline add-from-library, design system rework
**Researched:** 2026-04-02
**Confidence:** HIGH (based on full codebase inspection; all claims verified against source files)
---
## Standard Architecture
## Existing Architecture Baseline
### System Overview
This is a subsequent milestone. The section below describes what EXISTS today, then each sub-section documents exactly what changes and what stays the same.
The existing three-tier architecture (Pages → Hooks → Supabase) is sound and must be preserved. The UI overhaul introduces a new layer of **dashboard-specific view components** that sit between pages and the primitive shadcn/ui atoms. Nothing touches hooks or the library layer.
### Current System Layout
```
┌───────────────────────────────────────────────────────────────┐
Pages Layer
DashboardPage CategoriesPage BudgetDetailPage ...
(routing, data loading, layout composition)
───────────────────────────────────────────────────────────────┤
View Components Layer [NEW]
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ DashboardContent│ │CategorySection│ │ ChartPanel │
│ (hybrid layout) │ │(collapsible) │ │ (chart wrappers)│
└─────────────────┘ └──────────────┘ └─────────────────┘
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ SummaryStrip │ BudgetTable PageShell
│ (KPI cards row) │ │ (line items) │ │ (consistent
└─────────────────┘ └──────────────┘ │ header+CTA) │
└─────────────────┘
├───────────────────────────────────────────────────────────────┤
Primitive UI Layer (shadcn/ui)
Card Button Table Dialog Select Collapsible Badge ...
├───────────────────────────────────────────────────────────────┤
Hooks Layer [UNCHANGED]
useBudgets useBudgetDetail useCategories useAuth ...
───────────────────────────────────────────────────────────────┤
Library Layer [UNCHANGED] │
│ supabase.ts types.ts format.ts palette.ts utils.ts │
index.css (@theme tokens — EXTEND for new color tokens)
└───────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────
React 19 SPA (Vite + TypeScript)
Auth Layer App Layer (ProtectedRoute + AppLayout)
│ ┌───────────────┐ ┌─────────────────────────────────────────┐ │
│ LoginPage │ SidebarProvider > Sidebar > SidebarInset│
│ RegisterPage │ │ 6 nav items → 6 protected page routes │
└───────────────┘ └─────────────────────────────────────────┘
Pages (each uses PageShell + direct hook calls):
DashboardPage CategoriesPage TemplatePage BudgetListPage
BudgetDetailPage QuickAddPage SettingsPage
Hooks (TanStack Query v5, direct supabase-js client):
useAuth useCategories useTemplate useBudgets useQuickAdd
│ useMonthParam useBudgetDetail │
Design layer:
│ index.css @theme inline → OKLCH tokens → Tailwind 4 utility classes│
lib/palette.ts → categoryColors (CSS var references only)
shadcn/ui (radix-ui primitives + generated component files)
└─────────────────────────┬───────────────────────────────────────────
│ @supabase/supabase-js v2 (REST + RLS)
┌─────────────────────────▼───────────────────────────────────────────┐
Supabase (PostgreSQL 16 + Auth + Row Level Security)
│ Tables: profiles, categories, templates, template_items, │
│ budgets, budget_items, quick_add_items │
└─────────────────────────────────────────────────────────────────────┘
```
**The constraint is strict:** hooks and library are read-only during this milestone. All UI overhaul changes land in `src/pages/`, `src/components/`, and `src/index.css` only.
### Key Existing Patterns (Carry Forward Unchanged)
| Pattern | Location | Keep? |
|---------|----------|-------|
| PageShell header wrapper | `components/shared/PageShell.tsx` | YES |
| DashboardContent inner component keyed by budgetId | `DashboardPage.tsx` | YES — key prop pattern prevents stale state on month change |
| useMonthParam URL search param | `hooks/useMonthParam.ts` | YES |
| getOrCreateTemplate (auto-creates template row) | `hooks/useTemplate.ts` | YES |
| generateFromTemplate mutation | `hooks/useBudgets.ts` | YES — v2.0 triggers it automatically |
| Two-tier OKLCH color tokens (text ~0.55L, fill ~0.68L) | `index.css` | YES — values tuned, names unchanged |
| categoryColors CSS var references | `lib/palette.ts` | YES — names unchanged |
| Direction-aware diff (income under = bad, spending over = bad) | `BudgetDetailPage.tsx`, `CategorySection.tsx` | YES |
| TanStack Query cache invalidation per resource key | All mutation hooks | YES |
---
### Component Responsibilities
## v2.0 Architecture Changes
| Component | Responsibility | Typical Implementation |
|-----------|----------------|------------------------|
| `DashboardPage` | Find current month budget, render shell | Unchanged outer page; delegates to `DashboardContent` |
| `DashboardContent` | Hybrid layout orchestration | Calls `useBudgetDetail`; computes derived data with `useMemo`; renders SummaryStrip + charts + CategorySections |
| `SummaryStrip` | Three KPI cards (income, expenses, balance) | Grid of `StatCard` components; color-coded balance |
| `StatCard` | Single KPI display unit | shadcn Card with title, large number, optional trend indicator |
| `ChartPanel` | Houses all charts in responsive grid | Two-column grid on desktop: income bar chart left, expense donut right, spend horizontal bar full-width below |
| `IncomeBarChart` | Budgeted vs actual income vertical bar | Recharts `BarChart` wrapped in `ChartContainer` with `ChartConfig` |
| `ExpenseDonutChart` | Expense category breakdown donut | Recharts `PieChart` with `innerRadius`/`outerRadius` + custom legend |
| `SpendBarChart` | Horizontal budget vs actual by category type | Recharts `BarChart layout="vertical"` |
| `CategorySection` | Collapsible group for one category type | Radix `Collapsible.Root` wrapping a header row + `BudgetLineItems` |
| `CategorySectionHeader` | Always-visible row: type label, color dot, group totals, chevron | Trigger for the collapsible; shows budgeted/actual/diff inline |
| `BudgetLineItems` | Table of individual line items inside a section | shadcn `Table`; thin wrapper around existing `InlineEditCell` / `DifferenceCell` atoms |
| `PageShell` | Consistent page header with title + primary CTA | Reusable wrapper used by every page; enforces padding, heading size, CTA slot |
| `AppLayout` | Sidebar navigation shell | Minor visual refresh only; structure unchanged |
### 1. First-Run Detection and Wizard Routing
---
**Problem:** New users land on Dashboard with empty categories, no template, no budget. The blank state offers no guidance.
## Recommended Project Structure
**Solution:** A `useFirstRunState` hook detects first-run status. A `WizardRoute` guard in `App.tsx` redirects first-run users to `/setup` (a dedicated wizard page) before they can reach any other protected route.
The existing structure is well-organized. The overhaul adds a `dashboard/` subfolder and a `shared/` subfolder under components — no reorganization of hooks or lib.
**New hook — `src/hooks/useFirstRunState.ts`:**
```
src/
├── components/
│ ├── ui/ # shadcn primitives (do not modify)
│ │ └── collapsible.tsx # ADD — Radix Collapsible primitive
│ ├── dashboard/ # ADD — dashboard-specific view components
│ │ ├── DashboardContent.tsx # hybrid layout orchestrator
│ │ ├── SummaryStrip.tsx # KPI cards row
│ │ ├── StatCard.tsx # single KPI card
│ │ ├── ChartPanel.tsx # chart grid container
│ │ ├── IncomeBarChart.tsx # budgeted vs actual income bar
│ │ ├── ExpenseDonutChart.tsx # donut + legend
│ │ ├── SpendBarChart.tsx # horizontal budget vs actual
│ │ ├── CategorySection.tsx # collapsible category group
│ │ └── BudgetLineItems.tsx # line-item table inside section
│ ├── shared/ # ADD — cross-page reusable components
│ │ └── PageShell.tsx # consistent page header + CTA slot
│ ├── AppLayout.tsx # MODIFY — visual refresh only
│ └── QuickAddPicker.tsx # unchanged
├── pages/ # MODIFY — swap DashboardContent import; apply PageShell
│ ├── DashboardPage.tsx
│ ├── BudgetDetailPage.tsx
│ ├── BudgetListPage.tsx
│ ├── CategoriesPage.tsx
│ ├── TemplatePage.tsx
│ ├── QuickAddPage.tsx
│ ├── SettingsPage.tsx
│ ├── LoginPage.tsx
│ └── RegisterPage.tsx
├── hooks/ # UNCHANGED
├── lib/
│ ├── palette.ts # UNCHANGED — CSS vars already defined
│ └── ... # everything else unchanged
├── i18n/
│ ├── en.json # ADD new translation keys
│ └── de.json # ADD new translation keys
└── index.css # ADD semantic color tokens if needed
```
### Structure Rationale
- **`components/dashboard/`:** All dashboard-specific view components are co-located. They have no meaning outside the dashboard, so they do not belong in `shared/`. Avoids polluting the top-level components directory.
- **`components/shared/`:** `PageShell` is the one genuinely cross-page component introduced by this milestone. Keeping it separate signals that it is intentionally reusable, not page-specific.
- **`components/ui/collapsible.tsx`:** The Radix Collapsible primitive is not yet in the project (inspected file list confirms absence). It must be added via `npx shadcn@latest add collapsible` before building `CategorySection`.
---
## Architectural Patterns
### Pattern 1: Derived Data via `useMemo` in DashboardContent
**What:** All computed values — category group totals, chart data arrays, KPI numbers — are derived in one place (`DashboardContent`) using `useMemo`, then passed as plain props to presentational child components. Child components never call hooks or perform calculations themselves.
**When to use:** Any time a value depends on `items` array from `useBudgetDetail`. Centralizing derivation means one cache invalidation (after a budget item update) triggers one recalculation, and all children rerender from the same consistent snapshot.
**Trade-offs:** Slightly more props-passing verbosity. Benefit: children are trivially testable pure components.
**Example:**
```typescript
// DashboardContent.tsx
const { budget, items } = useBudgetDetail(budgetId)
const totals = useMemo(() => {
const income = items
.filter(i => i.category?.type === "income")
.reduce((sum, i) => sum + i.actual_amount, 0)
const expenses = items
.filter(i => i.category?.type !== "income")
.reduce((sum, i) => sum + i.actual_amount, 0)
return { income, expenses, balance: income - expenses + (budget?.carryover_amount ?? 0) }
}, [items, budget?.carryover_amount])
const groupedItems = useMemo(() =>
CATEGORY_TYPES.map(type => ({
type,
items: items.filter(i => i.category?.type === type),
budgeted: items.filter(i => i.category?.type === type).reduce((s, i) => s + i.budgeted_amount, 0),
actual: items.filter(i => i.category?.type === type).reduce((s, i) => s + i.actual_amount, 0),
})).filter(g => g.items.length > 0),
[items])
// Pass totals and groupedItems as props; child components are pure
// Returns { needsSetup: boolean, loading: boolean }
// needsSetup = true when: categories.length === 0 OR template_items.length === 0
// Uses existing useCategories() and useTemplate() — no new DB queries
export function useFirstRunState(): { needsSetup: boolean; loading: boolean }
```
### Pattern 2: Collapsible Category Sections via Radix Collapsible
**Route change — `src/App.tsx`:**
**What:** Each category group (income, bills, variable expenses, debt, savings, investment) is wrapped in a `Collapsible.Root`. The always-visible trigger row shows the category label, color dot, and group-level budget/actual/difference totals. The collapsible content reveals the individual line-item table.
```
// Add /setup route
// Add WizardRoute guard that wraps the existing ProtectedRoute subtree:
// if needsSetup && path !== "/setup" → redirect to /setup
// if !needsSetup && path === "/setup" → redirect to /
**When to use:** The dashboard hybrid view — users need the summary at a glance without scrolling through every line item. Opening a section is an explicit drill-down action.
<Route path="/setup" element={<ProtectedRoute><SetupWizardPage /></ProtectedRoute>} />
```
**Trade-offs:** Adds `open` state per section. Use `useState` per section (not global state — there are at most 6 sections). Do not persist open state in localStorage for v1; the sections should open fresh on each visit so the summary view is the default.
**Wizard state — local `useState` only in `SetupWizardPage`:**
**Example:**
```typescript
// CategorySection.tsx
import { Collapsible, CollapsibleContent, CollapsibleTrigger }
from "@/components/ui/collapsible"
import { ChevronDown } from "lucide-react"
import { useState } from "react"
interface CategorySectionProps {
type: CategoryType
budgeted: number
actual: number
items: BudgetItem[]
currency: string
}
export function CategorySection({ type, budgeted, actual, items, currency }: CategorySectionProps) {
const [open, setOpen] = useState(false)
const { t } = useTranslation()
return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger asChild>
<button className="flex w-full items-center gap-3 rounded-lg border px-4 py-3 hover:bg-muted/40">
<span className="size-3 shrink-0 rounded-full"
style={{ backgroundColor: categoryColors[type] }} />
<span className="font-medium">{t(`categories.types.${type}`)}</span>
<span className="ml-auto tabular-nums text-sm text-muted-foreground">
{formatCurrency(actual, currency)} / {formatCurrency(budgeted, currency)}
</span>
<ChevronDown className={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<BudgetLineItems items={items} currency={currency} type={type} />
</CollapsibleContent>
</Collapsible>
)
type WizardState = {
step: 1 | 2 | 3
selectedPresets: string[] // preset IDs chosen in step 1
amounts: Record<string, number> // presetId → budgeted amount (step 2)
}
```
### Pattern 3: shadcn ChartContainer + ChartConfig for All Charts
Wizard state is ephemeral. It lives in the page component and is discarded on unmount. No context, no global store.
**What:** Wrap every Recharts chart in shadcn's `ChartContainer` component. Define colors and labels in a `ChartConfig` object that references existing CSS variable tokens from `index.css` (`var(--color-income)`, `var(--color-bill)`, etc.). Do not hardcode hex values inside chart components.
**Completion flow:** `handleFinish()` in `SetupWizardPage` runs:
1. `useCategories().create` × N (create one category row per selected preset)
2. `useTemplate().createItem` × N (create template items, each linked to the new categories)
3. `navigate("/")` — DashboardPage loads, `useFirstRunState` returns `needsSetup=false`
**When to use:** All three chart types (bar, horizontal bar, donut). This ensures charts automatically theme with the design system and dark mode works at zero extra cost.
**Preset data — `src/data/presets.ts`:** Static array of common budget items (rent, salary, groceries, car insurance, etc.) with suggested amounts and category types. No DB table needed — this is compile-time data.
**Trade-offs:** Requires adding the shadcn `chart` component (`npx shadcn@latest add chart`). Minor wrapper overhead, but the CSS variable binding and tooltip consistency is worth it.
**No new DB migration.** Wizard writes to existing `categories` and `template_items` tables.
**New files:**
- `src/hooks/useFirstRunState.ts`
- `src/pages/SetupWizardPage.tsx`
- `src/components/wizard/WizardStep.tsx`
- `src/components/wizard/CategoryDefaults.tsx`
- `src/components/wizard/TemplateDefaults.tsx`
- `src/data/presets.ts`
**Modified files:**
- `src/App.tsx` — add `/setup` route + WizardRoute guard
---
### 2. Auto-Budget Creation on Month Visit
**Problem:** Visiting the dashboard for a new month shows an empty state with two manual buttons ("Create Budget", "Generate from Template"). This friction contradicts the "just works" goal.
**Solution:** Auto-trigger `generateFromTemplate` inside a `useEffect` in `DashboardPage` when no budget exists for the current month and the user's template has items.
**Integration point — `src/pages/DashboardPage.tsx`:**
Add `useTemplate()` call alongside the existing `useBudgets()` call. Add a `useEffect` with these guards:
**Example:**
```typescript
// IncomeBarChart.tsx
import { ChartContainer, ChartTooltip, ChartTooltipContent }
from "@/components/ui/chart"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"
const { items: templateItems } = useTemplate()
const hasTemplateItems = templateItems.length > 0
const isCurrentMonth = month === currentMonth // currentMonth from useMonthParam baseline
const attempted = useRef(false)
const chartConfig = {
budgeted: { label: "Budgeted", color: "var(--color-income)" },
actual: { label: "Actual", color: "var(--color-income)" },
} satisfies ChartConfig
// data: [{ month: "March", budgeted: 3000, actual: 2850 }]
export function IncomeBarChart({ data, currency }: Props) {
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" />
<YAxis tickFormatter={v => formatCurrency(v, currency)} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="budgeted" fill="var(--color-income)" radius={4} />
<Bar dataKey="actual" fill="var(--color-income)" fillOpacity={0.6} radius={4} />
</BarChart>
</ChartContainer>
)
}
useEffect(() => {
if (
!loading &&
!currentBudget &&
hasTemplateItems &&
isCurrentMonth &&
!attempted.current &&
!generateFromTemplate.isPending
) {
attempted.current = true
generateFromTemplate.mutate({ month: parsedMonth, year: parsedYear, currency })
}
}, [loading, currentBudget, hasTemplateItems, isCurrentMonth])
```
### Pattern 4: PageShell for Consistent Page Headers
**Currency source:** The `generateFromTemplate` mutation currently accepts `currency` as a param. Pull the user's preferred currency from the profile via a `useProfile` hook or extend `useAuth` to expose it. If `useProfile` is not yet implemented, fall back to reading from the most recent budget's `currency` field, or default to the settings page value.
**What:** A `PageShell` component accepts `title`, `description` (optional), and `action` (optional ReactNode slot for a primary CTA button). Every page wraps its top section in `PageShell`. This enforces a consistent heading size, spacing, and CTA placement across the entire app refresh.
**No new API endpoint.** The `generateFromTemplate` mutation in `useBudgets.ts` already handles the full create + seed flow server-side. This is a pure frontend trigger change.
**When to use:** All 9 pages in the overhaul. Any new page added in future milestones should also use it.
**Edge cases handled by the guards:**
- Template has 0 items → `hasTemplateItems=false` → skip auto-create, show prompt to configure template
- Budget already exists → `!currentBudget=false` → skip
- Past/future month navigation → `isCurrentMonth=false` → skip (only auto-create for today's month)
- Multiple renders before mutation completes → `attempted` ref prevents double-fire
**Trade-offs:** Adds one wrapper per page. The benefit is that a single visual change to the page header propagates everywhere without hunting through 9 files.
**Modified files:**
- `src/pages/DashboardPage.tsx` — add useTemplate(), useEffect trigger, currency source
---
### 3. Inline Add-from-Category-Library (Replaces QuickAdd Page)
**Problem:** QuickAdd is a separate nav page (`/quick-add`), a separate table (`quick_add_items`), and a Popover on the Dashboard. Three disconnected surfaces for one concept. The mental model is unclear.
**Solution:** Replace the QuickAdd Popover with an `AddOneOffSheet` component (a shadcn Sheet/side panel) available directly on BudgetDetailPage and Dashboard. It shows the user's category list grouped by type. The user picks a category, enters an amount, and a `one_off` budget item is created. The `quick_add_items` table remains intact but is removed from the primary workflow.
**New component — `src/components/budget/AddOneOffSheet.tsx`:**
**Example:**
```typescript
// shared/PageShell.tsx
interface PageShellProps {
title: string
description?: string
action?: React.ReactNode
children: React.ReactNode
interface AddOneOffSheetProps {
budgetId: string
open: boolean
onOpenChange: (open: boolean) => void
}
// Uses: useCategories() + useBudgets().createItem
// On save: createItem.mutate({ budgetId, category_id, budgeted_amount, actual_amount, notes, item_tier: "one_off" })
```
export function PageShell({ title, description, action, children }: PageShellProps) {
return (
<div className="flex flex-col gap-6">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p>
)}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
{children}
</div>
)
}
This replaces `QuickAddPicker.tsx` at both call sites. The existing `useBudgets().createItem` mutation is unchanged — it already inserts `item_tier: "one_off"` budget items.
**QuickAdd page fate:** Remove from `AppLayout.tsx` nav items array. Keep the route and page file in place (backwards compat, existing data). Do NOT drop the `quick_add_items` table or `useQuickAdd` hook.
**Modified files:**
- `src/components/AppLayout.tsx` — remove QuickAdd from `navItems` array
- `src/pages/DashboardPage.tsx` — replace `<QuickAddPicker>` with `<AddOneOffSheet>`
- `src/pages/BudgetDetailPage.tsx` — replace `<QuickAddPicker>` with `<AddOneOffSheet>`
**New files:**
- `src/components/budget/AddOneOffSheet.tsx`
**No DB migration needed.** `one_off` budget items already exist in `budget_items` with `item_tier = 'one_off'`.
---
### 4. Dashboard Simplification
**Problem:** The current dashboard renders three chart types (donut + two bar charts) in a 3-column grid plus collapsible sections. This is dense and the charts don't clearly reflect user-entered data. v2.0 goal: "this month's budget at a glance."
**Solution:** Remove the 3-column chart grid from `DashboardContent`. Keep `SummaryStrip` (3 KPI cards) and `CollapsibleSections`. The chart components stay in the codebase — they are correct — but are removed from the Dashboard layout.
**Modified files:**
- `src/pages/DashboardPage.tsx` — remove chart grid `<div>` and the three chart imports from `DashboardContent`
**Dashboard data correctness:** The aggregation logic (`totalIncome`, `totalExpenses`, `budgetedIncome`, `budgetedExpenses`) is already correct in `DashboardContent`. The perceived "data not reflecting input" issue is caused by the auto-create flow not existing yet — once a budget exists and items are populated, the existing calculations are accurate. No aggregation logic changes needed.
**Empty state after removing charts:** The removed chart area frees vertical space. Collapsible sections become the primary view. The `SummaryStrip` at the top provides the quick-glance summary. Consider adding a single "at a glance" progress indicator to `SummaryStrip` to compensate for the removed chart density.
---
### 5. Design System Token Rework
**Problem:** `--radius: 0.625rem` produces rounded corners everywhere. The palette has high chroma values that read as saturated rather than pastel.
**Solution:** Lower the radius token and tune OKLCH lightness/chroma in `index.css`. All shadcn/ui components reference `--radius` via `rounded-[--radius]` — a single value change propagates everywhere.
**The only changed file: `src/index.css`**
Key changes:
```css
/* Sharp edges */
--radius: 0.125rem; /* was 0.625rem */
/* Background: warmer, closer to pure white */
--color-background: oklch(0.99 0.003 260); /* was 0.98 0.005 260 */
/* Softer border */
--color-border: oklch(0.91 0.008 260); /* was 0.88 0.01 260 */
/* Category pastels: increase lightness slightly, keep chroma */
/* e.g. --color-income-fill: oklch(0.72 0.14 155) → 0.78 0.12 155 */
```
**What does NOT change:**
- Token names (e.g., `--color-income`, `--color-bill`) — `palette.ts` references these by name
- Two-tier pattern (text tokens at ~0.55L, fill tokens at ~0.680.78L)
- Category color identities (income=green, bill=orange-red, etc.)
- The `@theme inline` structure
**shadcn component customization:** Zero component-file changes required. Radius change propagates automatically. Color changes propagate via CSS var resolution.
---
## Updated System Layout (v2.0)
```
┌─────────────────────────────────────────────────────────────────────┐
│ React 19 SPA │
│ │
│ Auth Layer App Layer │
│ ┌───────────────┐ ┌─────────────────────────────────────────┐ │
│ │ LoginPage │ │ WizardRoute (NEW) │ │
│ │ RegisterPage │ │ └─ ProtectedRoute + AppLayout (MOD) │ │
│ └───────────────┘ │ Sidebar (5 nav items, -QuickAdd) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ New route /setup: │
│ SetupWizardPage (NEW) → 3 steps → writes categories + template │
│ │
│ Modified pages: │
│ DashboardPage (MOD: auto-create, -charts, AddOneOffSheet) │
│ BudgetDetailPage (MOD: AddOneOffSheet replaces QuickAddPicker) │
│ │
│ Unchanged pages: │
│ CategoriesPage TemplatePage BudgetListPage SettingsPage │
│ QuickAddPage (hidden from nav, route kept) │
│ │
│ New components: │
│ wizard/WizardStep wizard/CategoryDefaults wizard/TemplateDefaults│
│ budget/AddOneOffSheet │
│ │
│ New hooks: │
│ useFirstRunState │
│ │
│ New data: │
│ src/data/presets.ts (static, no DB) │
└─────────────────────────┬───────────────────────────────────────────┘
│ @supabase/supabase-js v2 (unchanged)
┌─────────────────────────▼───────────────────────────────────────────┐
│ Supabase (unchanged schema, no new migrations) │
│ All new features write to existing tables │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Data Flow
### Dashboard Read Flow
## Component Boundary Map (v2.0)
```
DashboardPage renders
useBudgets() → finds current month budget → passes budgetId prop
DashboardContent mounts
useBudgetDetail(budgetId) → TanStack Query cache or Supabase fetch
↓ data arrives
useMemo recalculates: totals, groupedItems, chartData
Props flow DOWN to pure presentational children:
SummaryStrip(totals)
ChartPanel(chartData, currency)
├── IncomeBarChart(barData)
├── ExpenseDonutChart(pieData)
── SpendBarChart(horizontalData)
CategorySection[] (groupedItems, per-type items)
── BudgetLineItems(items, currency)
App.tsx
├── /login → LoginPage
├── /register → RegisterPage
├── /setup → ProtectedRoute > SetupWizardPage (NEW)
│ ├── WizardStep (NEW)
├── step 1: CategoryDefaults (NEW) — checkboxes from presets.ts
│ └── step 2: TemplateDefaults (NEW) — amount inputs per selected preset
└── /* → WizardRoute > ProtectedRoute > AppLayout (MODIFIED: -QuickAdd nav)
├── / → DashboardPage (MODIFIED)
│ ├── MonthNavigator
│ ├── DashboardContent (MODIFIED: no chart grid)
│ │ ├── SummaryStrip (unchanged)
│ │ ├── CollapsibleSections (unchanged)
│ │ └── AddOneOffSheet (NEW, replaces QuickAddPicker)
├── /categories → CategoriesPage (unchanged)
── /template → TemplatePage (unchanged)
├── /budgets → BudgetListPage (unchanged)
── /budgets/:id → BudgetDetailPage (MODIFIED)
│ └── AddOneOffSheet (NEW, replaces QuickAddPicker)
├── /quick-add → QuickAddPage (hidden from nav, route kept)
└── /settings → SettingsPage (unchanged)
```
### Budget Item Edit Flow (unchanged, flows back up)
---
## Data Flow Changes
### First-Run + Wizard Completion Flow
```
InlineEditCell: user types new actual_amount
New user lands on any protected route
onCommit → updateItem.mutateAsync({ id, budgetId, actual_amount })
WizardRoute: useFirstRunState() → needsSetup=true
Supabase updates budget_items row
redirect /setup
onSuccess: queryClient.invalidateQueries(["budgets", budgetId, "items"])
SetupWizardPage renders (step 1)
User checks preset categories (Rent, Salary, Groceries, ...)
↓ [Next]
step 2: User adjusts amounts per selected category
↓ [Finish] → handleFinish()
useBudgetDetail re-fetches items
useCategories().create × N → INSERT categories (N rows)
↓ await
useTemplate().createItem × N → INSERT template_items (N rows)
↓ await
navigate("/")
DashboardContent useMemo recalculates all derived values
ALL children rerender with consistent new data
DashboardPage: useFirstRunState() → needsSetup=false
Auto-create effect fires (see below)
```
### State Management (what lives where)
### Auto-Budget Creation Flow
| State | Location | Why |
|-------|----------|-----|
| Budget and items data | TanStack Query cache | Server state, must survive component unmounts |
| Collapsible open/closed | `useState` in each `CategorySection` | Purely local UI state; 6 booleans maximum |
| Chart tooltip hover | Recharts internal | Library-managed interaction state |
| Dialog open/closed | `useState` in page components | Unchanged from current pattern |
| Currency, locale | `Profile` via Supabase → hooks | Read from `budget.currency`; no separate state |
```
DashboardPage mounts (month = "2026-04", current month)
useBudgets() → budgets list → currentBudget = undefined
useTemplate() → items → hasTemplateItems = true
useEffect fires:
!loading && !currentBudget && hasTemplateItems && isCurrentMonth && !attempted
generateFromTemplate.mutate({ month: 4, year: 2026, currency: "EUR" })
Supabase:
1. INSERT budgets (start_date=2026-04-01, end_date=2026-04-30)
2. SELECT template_items WHERE template_id = user's template
3. INSERT budget_items × N (actual_amount=0 per item)
onSuccess: invalidate ["budgets"], ["budgets", id], ["budgets", id, "items"]
DashboardPage rerenders: currentBudget = new budget
DashboardContent mounts (keyed by budgetId)
SummaryStrip + CollapsibleSections render with template-populated data
```
### Inline Add-One-Off Flow
```
DashboardPage or BudgetDetailPage
"Add one-off" button → AddOneOffSheet opens (Sheet side panel)
useCategories() provides category list (grouped by type)
User picks category, enters amount, optional notes
[Add] → useBudgets().createItem.mutate({
budgetId,
category_id,
budgeted_amount: amount,
actual_amount: amount, // one-off: budget = actual at time of entry
notes,
item_tier: "one_off"
})
onSuccess: invalidate ["budgets", budgetId, "items"]
Sheet closes → item appears in appropriate CollapsibleSection
```
---
## Scaling Considerations
## New vs Modified File Summary
This is a personal finance app for a single authenticated user at a time. Scale is not a concern for rendering. The relevant concern is **perceived performance** on the dashboard when `items` is large (100+ line items).
### New Files
| Scale | Architecture Adjustment |
|-------|--------------------------|
| <50 items (normal) | No optimization needed — current useMemo pattern is fast |
| 50-200 items | useMemo already handles this — O(n) passes are negligible |
| 200+ items | Consider React.memo on CategorySection to skip unchanged sections; still no virtualization needed |
| Dashboard load time | TanStack Query 5-min staleTime means instant rerender on navigate-back; no change required |
| File | Purpose |
|------|---------|
| `src/hooks/useFirstRunState.ts` | Detect first-run: categories=0 OR template_items=0 |
| `src/data/presets.ts` | Static preset category list with suggested amounts |
| `src/pages/SetupWizardPage.tsx` | Multi-step first-run wizard (local state, 3 steps) |
| `src/components/wizard/WizardStep.tsx` | Step wrapper with step indicator / progress |
| `src/components/wizard/CategoryDefaults.tsx` | Preset category checkbox picker (step 1) |
| `src/components/wizard/TemplateDefaults.tsx` | Amount inputs per selected preset (step 2) |
| `src/components/budget/AddOneOffSheet.tsx` | Sheet-based one-off item adder (replaces QuickAddPicker) |
### Modified Files
| File | What Changes |
|------|-------------|
| `src/App.tsx` | Add `/setup` route; add WizardRoute guard |
| `src/components/AppLayout.tsx` | Remove QuickAdd from `navItems` array |
| `src/pages/DashboardPage.tsx` | Add useTemplate(); add auto-create useEffect; remove chart grid; swap AddOneOffSheet for QuickAddPicker |
| `src/pages/BudgetDetailPage.tsx` | Swap AddOneOffSheet for QuickAddPicker |
| `src/index.css` | Lower `--radius`; tune OKLCH pastel token values |
| `src/i18n/en.json` | Add wizard i18n keys; remove/rename quick-add nav key |
| `src/i18n/de.json` | Same additions as en.json (bilingual requirement) |
### Unchanged Files (confirmed)
`useBudgets.ts`, `useTemplate.ts`, `useCategories.ts`, `useQuickAdd.ts`, `useAuth.ts`, `useMonthParam.ts`, `lib/types.ts`, `lib/palette.ts`, `lib/format.ts`, `lib/supabase.ts` — all hooks and library files are read-only for this milestone.
---
## Anti-Patterns
## Build Order (Dependency-Aware)
### Anti-Pattern 1: Deriving Chart Data Inside Chart Components
Phase dependencies must be respected. Build bottom-up.
**What people do:** Put `items.filter(...).reduce(...)` directly inside `IncomeBarChart` or `ExpenseDonutChart`, passing the raw `items` array from `useBudgetDetail` as a prop.
**Phase 1: Design Tokens (no deps)**
- `src/index.css` — lower `--radius`, refine OKLCH pastel values
- Verify all 9 existing pages render correctly; no logic changes
- Fast and reversible — do this first to establish visual baseline
**Why it's wrong:** Each chart component recalculates from scratch. If `items` reference changes (after a mutation), all three charts recalculate independently. Chart components become impure, harder to test, and cannot be reused with different data shapes without changing their internals.
**Phase 2: Preset Data + First-Run Hook (no deps)**
- `src/data/presets.ts` — static data, no imports needed
- `src/hooks/useFirstRunState.ts` — depends on existing `useCategories` + `useTemplate`; no UI yet
**Do this instead:** Derive all chart data in `DashboardContent` with `useMemo`. Pass prepared `barData`, `pieData`, `horizontalData` arrays to each chart. Charts receive typed data arrays and render only.
**Phase 3: Setup Wizard (depends on Phase 2)**
- `src/components/wizard/WizardStep.tsx` — pure presentational
- `src/components/wizard/CategoryDefaults.tsx` — depends on presets.ts
- `src/components/wizard/TemplateDefaults.tsx` — depends on presets.ts
- `src/pages/SetupWizardPage.tsx` — assembles wizard components + useFirstRunState + mutation hooks
- `src/App.tsx` — wire `/setup` route + WizardRoute guard
**Phase 4: Auto-Budget Creation (depends on Phase 3 — template must be populatable)**
- `src/pages/DashboardPage.tsx` — add `useTemplate()` import + auto-create `useEffect`
- Test: wizard → dashboard → budget auto-creates → SummaryStrip shows template data
**Phase 5: Inline Add-One-Off + Dashboard Simplification (depends on Phase 4)**
- `src/components/budget/AddOneOffSheet.tsx` — depends on `useCategories` + `useBudgets` (both existing)
- `src/pages/DashboardPage.tsx` — remove chart grid; wire AddOneOffSheet
- `src/pages/BudgetDetailPage.tsx` — wire AddOneOffSheet
- `src/components/AppLayout.tsx` — remove QuickAdd nav item
---
### Anti-Pattern 2: Hardcoding Colors Inside Chart Components
## Anti-Patterns to Avoid
**What people do:** Paste hex values like `fill="#4ade80"` into `<Bar>` and `<Cell>` components to match the design.
### Anti-Pattern 1: Global State for Wizard
**Why it's wrong:** The existing `index.css` already defines category colors as OKLCH CSS variables (`--color-income`, `--color-bill`, etc.) and `palette.ts` maps them to `var(--color-income)` etc. Hardcoding breaks dark mode adaptation, creates a second source of truth, and means a palette change requires editing multiple files.
**What people do:** Store wizard state in a React context, Zustand store, or TanStack Query mutation cache.
**Why it's wrong:** Wizard runs once, on first login. Global state persists unnecessarily and creates cleanup complexity.
**Do this instead:** Local `useState` in `SetupWizardPage`. When the page unmounts (after `navigate("/")`), state is automatically cleaned up.
**Do this instead:** Always reference `categoryColors[type]` from `palette.ts` (which returns the CSS variable string) for both chart fills and UI color dots. For shadcn `ChartContainer`, pass `color: categoryColors[type]` in the `ChartConfig` object.
### Anti-Pattern 2: Auto-Create Without Template Guard
**What people do:** Trigger `generateFromTemplate` whenever `!currentBudget`, regardless of template state.
**Why it's wrong:** If a user skips the wizard or has an empty template, an empty budget gets silently created. This is worse than the empty state — it looks like a bug, not a fresh start.
**Do this instead:** Guard with `hasTemplateItems && isCurrentMonth`. Show a helpful CTA to configure the template if template is empty.
### Anti-Pattern 3: Dropping the quick_add_items Table
**What people do:** Add a migration to drop `quick_add_items` when the QuickAdd nav item is removed.
**Why it's wrong:** Existing users have data there. The DB migration is irreversible. The table costs nothing to keep.
**Do this instead:** Remove the nav item and surface only. Leave the table, `useQuickAdd` hook, and `/quick-add` route in place. Schedule a cleanup migration in a future milestone after confirming no users rely on it.
### Anti-Pattern 4: Renaming Design Token CSS Variables
**What people do:** Rename tokens during the design rework (e.g., `--color-income``--category-color-income`) to make the naming more semantic.
**Why it's wrong:** `palette.ts` references token names as strings: `var(--color-income)`. All chart components use `var(--color-income-fill)`. Renaming causes silent CSS failures — no TypeScript error, no build error, just missing colors.
**Do this instead:** Change only the VALUES of existing tokens. Add new token names only for net-new concepts.
### Anti-Pattern 5: Wizard as Modal Overlay
**What people do:** Show the wizard as a Dialog or Sheet over the main app layout on first load.
**Why it's wrong:** No URL, so refresh drops the user back to the start or the main app with incomplete setup. Focus management in a full-screen modal is complex. Back button behavior is broken.
**Do this instead:** Dedicated `/setup` route. `WizardRoute` handles redirect logic. The browser URL reflects wizard state, refresh works, and no special cleanup is needed.
### Anti-Pattern 6: Adding useEffect Dependencies by Feel
**What people do:** Add `generateFromTemplate` itself to the `useEffect` deps array.
**Why it's wrong:** Mutation objects from TanStack Query are re-created on every render, causing the effect to re-run in a loop.
**Do this instead:** Use a `useRef` guard (`attempted.current`) and gate on stable primitive values (`!loading`, `!currentBudget`, `hasTemplateItems`). Do not include the mutation object in the dependency array.
---
### Anti-Pattern 3: One Monolithic DashboardContent Component
## Supabase / DB Notes (v2.0)
**What people do:** Add all new dashboard sections — summary cards, three charts, six collapsible sections, QuickAdd button — directly into one large `DashboardContent.tsx` that becomes 400+ lines.
**No new migrations required.** All v2.0 features write to existing tables:
**Why it's wrong:** The existing `DashboardContent` is already 200 lines with just two charts and progress bars. A full hybrid dashboard with three chart types and six collapsible sections will exceed 600 lines inline, making it impossible to review, test, or modify individual sections without reading the whole file.
| Feature | Writes To | Via |
|---------|-----------|-----|
| Wizard | `categories`, `template_items` | existing `useCategories().create`, `useTemplate().createItem` |
| Auto-budget | `budgets`, `budget_items` | existing `useBudgets().generateFromTemplate` |
| Inline one-off | `budget_items` | existing `useBudgets().createItem` |
| Design tokens | — | CSS only |
**Do this instead:** Extract into the component tree defined above. `DashboardContent` orchestrates layout and owns derived data. Each distinct visual section (`SummaryStrip`, `ChartPanel`, `CategorySection`) is its own file. The rule: if a block of JSX has a distinct visual purpose, it gets its own component.
The `quick_add_items` table becomes read-only from the primary UI flow. No rows deleted, no schema change.
---
### Anti-Pattern 4: Using shadcn Accordion Instead of Collapsible for Category Sections
## Integration Points: New vs Existing
**What people do:** Reach for the `Accordion` component (which is already in some shadcn setups) to build collapsible category sections because it looks similar.
**Why it's wrong:** Accordion by default enforces "only one open at a time" (type="single") or requires explicit `type="multiple"` with `collapsible` prop to allow free open/close. For budget sections, users may want to compare two categories side-by-side with both open simultaneously. Using individual `Collapsible` per section gives full independent control without fighting Accordion's root-state coordination.
**Do this instead:** Use Radix `Collapsible` (shadcn wrapper) on each `CategorySection` with independent `useState`. Six independent booleans are trivially managed.
---
### Anti-Pattern 5: Modifying Hooks or Supabase Queries
**What people do:** Add derived fields or aggregations to the `useBudgetDetail` return value, or add new Supabase query fields, because it seems convenient during the UI overhaul.
**Why it's wrong:** Explicitly out of scope (PROJECT.md: "No Supabase schema changes — UI-only modifications"). Any hook changes risk breaking `BudgetDetailPage` and other consumers. The data model is sufficient — all needed aggregations can be computed with `useMemo` from the existing `items` array.
**Do this instead:** Keep hooks read-only. All computation lives in the presentation layer via `useMemo`.
---
## Integration Points
### External Services
| Service | Integration | Notes |
|---------|-------------|-------|
| Supabase | Unchanged — hooks layer handles all DB calls | No new RPC calls, no schema changes |
| Recharts | Chart primitives — wrap with `ChartContainer` | Requires adding shadcn `chart` component |
| Radix UI | `Collapsible` primitive — add via shadcn CLI | Not yet in project; must be added before CategorySection work |
| i18next | All new UI text needs keys in `en.json` and `de.json` | Add keys before rendering any new text; no runtime fallback acceptable |
### Internal Boundaries
| Boundary | Communication | Notes |
|----------|---------------|-------|
| `DashboardContent` ↔ chart components | Props only — typed data arrays + currency string | Charts are pure; they do not call hooks |
| `DashboardContent``CategorySection` | Props only — grouped items, budgeted/actual totals, currency, mutation handlers | Mutation handlers passed down from `DashboardContent` which owns `useBudgets()` mutations |
| `CategorySection``BudgetLineItems` | Props only — items array, currency, type | `BudgetLineItems` is a thin table wrapper; all mutations are callbacks from parent |
| `PageShell` ↔ all pages | Props (title, action slot, children) | No state shared; purely compositional |
| `index.css` @theme tokens ↔ components | CSS variables via `var(--color-X)` and `palette.ts` | Single source of truth for all color; never duplicate in component style props |
---
## Build Order Implications
Components have dependencies that dictate implementation order:
1. **`components/ui/collapsible.tsx`** — Add via `npx shadcn@latest add collapsible`. Required by `CategorySection`. Do first.
2. **`components/ui/chart.tsx`** — Add via `npx shadcn@latest add chart`. Required by all chart components. Do first.
3. **`shared/PageShell.tsx`** — No dependencies. Build early; apply to all pages as each is refreshed.
4. **`StatCard` + `SummaryStrip`** — Only depends on `formatCurrency` and Tailwind. Build second after primitives.
5. **Chart components** (`IncomeBarChart`, `ExpenseDonutChart`, `SpendBarChart`) — Depend on `ChartContainer`. Build after step 2.
6. **`ChartPanel`** — Composes the three chart components. Build after step 5.
7. **`BudgetLineItems`** — Refactored from existing `BudgetDetailPage` table code; existing `InlineEditCell` and `DifferenceCell` are reusable as-is.
8. **`CategorySection`** — Depends on `Collapsible` primitive and `BudgetLineItems`. Build after steps 1 and 7.
9. **`DashboardContent`** — Orchestrates everything. Build last, wiring together all children. Replace the existing `DashboardContent` function in `DashboardPage.tsx`.
10. **Non-dashboard page refreshes** — Apply `PageShell` + visual refresh to remaining 7 pages. Independent of dashboard work; can be done in any order.
| New Feature | Existing Hooks/Mutations Used | New Additions |
|-------------|------------------------------|---------------|
| First-run wizard | `useCategories().create`, `useTemplate().createItem` | `useFirstRunState`, `presets.ts`, wizard components |
| Auto-budget creation | `useBudgets().generateFromTemplate` (already exists) | `useEffect` trigger in DashboardPage, `hasTemplateItems` guard |
| Inline one-off add | `useBudgets().createItem` (already exists), `useCategories()` | `AddOneOffSheet` component |
| Dashboard simplification | `useBudgetDetail`, `useMonthParam`, `SummaryStrip`, `CollapsibleSections` | Remove 3-column chart grid from DashboardContent |
| Design tokens | All existing components consume tokens automatically | Token value adjustments in `index.css` only |
---
## Sources
- Radix UI Collapsible: https://www.radix-ui.com/primitives/docs/components/collapsible (HIGH confidence — official docs)
- shadcn/ui Chart component: https://ui.shadcn.com/docs/components/radix/chart (HIGH confidence — official docs)
- shadcn/ui Accordion: https://ui.shadcn.com/docs/components/radix/accordion (HIGH confidence — official docs)
- Tailwind CSS v4 @theme tokens: https://tailwindcss.com/docs/theme (HIGH confidence — official docs)
- React useMemo: https://react.dev/reference/react/useMemo (HIGH confidence — official docs)
- Existing codebase: inspected `src/` fully — `DashboardPage.tsx`, `BudgetDetailPage.tsx`, `CategoriesPage.tsx`, `AppLayout.tsx`, `palette.ts`, `types.ts`, `index.css`, `useBudgets.ts` (HIGH confidence — primary source)
- Full codebase inspection (April 2026):
- `src/App.tsx`, `src/components/AppLayout.tsx`
- `src/pages/DashboardPage.tsx`, `src/pages/TemplatePage.tsx`, `src/pages/BudgetDetailPage.tsx`
- `src/hooks/useTemplate.ts`, `src/hooks/useBudgets.ts`, `src/hooks/useQuickAdd.ts`, `src/hooks/useMonthParam.ts`
- `src/components/QuickAddPicker.tsx`, `src/components/shared/PageShell.tsx`
- `src/lib/types.ts`, `src/lib/palette.ts`
- `src/index.css`, `src/i18n/en.json`
- `supabase/migrations/002_categories.sql` through `005_quick_add.sql`
- Confidence: HIGH — all claims are grounded in verified source code, not assumptions
---
*Architecture research for: SimpleFinanceDash UI overhaul — React + Tailwind + shadcn/ui + Recharts*
*Researched: 2026-03-16*
*Architecture research for: SimpleFinanceDash v2.0 — wizard setup, auto-budget, inline add, design rework*
*Researched: 2026-04-02*