docs(02): create phase plan — 3 plans across 2 waves for dashboard charts and layout

This commit is contained in:
2026-03-16 12:57:33 +01:00
parent e0b3194211
commit dca5b04494
4 changed files with 759 additions and 6 deletions

View File

@@ -37,21 +37,22 @@ Plans:
- [x] 01-02-PLAN.md — Build PageShell, StatCard, SummaryStrip, DashboardSkeleton and integrate into DashboardPage
### Phase 2: Dashboard Charts and Layout
**Goal**: Deliver the full dashboard chart suite — donut, vertical bar, and horizontal bar — inside a responsive ChartPanel, with month navigation and memoized data derivations
**Goal**: Deliver the full dashboard chart suite — donut, vertical bar, and horizontal bar — inside a responsive 3-column layout, with month navigation and memoized data derivations
**Depends on**: Phase 1
**Requirements**: UI-DASH-01, UI-BAR-01, UI-HBAR-01, UI-DONUT-01
**Research flag**: No — Recharts 3.8.0 chart implementations and the `chart.tsx` fix are fully documented
**Research flag**: No — Recharts 2.15.4 chart implementations and the `chart.tsx` fix are fully documented
**Success Criteria** (what must be TRUE):
1. Dashboard displays an expense donut chart with center total label, active sector hover expansion, and a custom legend — replacing the existing flat pie chart
2. Dashboard displays a grouped vertical bar chart comparing income budgeted vs actual amounts
3. Dashboard displays a horizontal bar chart comparing budget vs actual spending by category type
4. All three charts consume colors from CSS variable tokens (no hardcoded hex values) and render correctly with zero-item budgets (empty state)
5. User can navigate between budget months on the dashboard without leaving the page, and all charts and cards update to reflect the selected month
**Plans**: TBD
**Plans**: 3 plans
Plans:
- [ ] 02-01: TBD
- [ ] 02-02: TBD
- [ ] 02-01-PLAN.md — Month navigation infrastructure (useMonthParam hook, MonthNavigator, ChartEmptyState, i18n keys)
- [ ] 02-02-PLAN.md — Three chart components (ExpenseDonutChart, IncomeBarChart, SpendBarChart)
- [ ] 02-03-PLAN.md — Dashboard integration (wire charts + month nav into DashboardPage, update skeleton)
### Phase 3: Collapsible Dashboard Sections
**Goal**: Complete the dashboard hybrid view with collapsible per-category sections that show individual line items, group totals, and variance indicators
@@ -133,6 +134,6 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Design Foundation and Primitives | 2/2 | Complete | 2026-03-16 |
| 2. Dashboard Charts and Layout | 0/TBD | Not started | - |
| 2. Dashboard Charts and Layout | 0/3 | Not started | - |
| 3. Collapsible Dashboard Sections | 0/TBD | Not started | - |
| 4. Full-App Design Consistency | 0/TBD | Not started | - |

View File

@@ -0,0 +1,207 @@
---
phase: 02-dashboard-charts-and-layout
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/hooks/useMonthParam.ts
- src/components/dashboard/MonthNavigator.tsx
- src/components/dashboard/charts/ChartEmptyState.tsx
- src/i18n/en.json
- src/i18n/de.json
autonomous: true
requirements: [UI-DASH-01]
must_haves:
truths:
- "useMonthParam hook reads month from URL search params and falls back to current month"
- "MonthNavigator renders prev/next arrows and a dropdown listing all budget months"
- "Navigating months updates URL without page reload"
- "ChartEmptyState renders a muted placeholder with message text inside a chart card"
- "i18n keys exist for month navigation and chart labels in both EN and DE"
artifacts:
- path: "src/hooks/useMonthParam.ts"
provides: "Month URL state hook"
exports: ["useMonthParam"]
- path: "src/components/dashboard/MonthNavigator.tsx"
provides: "Month navigation UI with arrows and dropdown"
exports: ["MonthNavigator"]
- path: "src/components/dashboard/charts/ChartEmptyState.tsx"
provides: "Shared empty state placeholder for chart cards"
exports: ["ChartEmptyState"]
key_links:
- from: "src/hooks/useMonthParam.ts"
to: "react-router-dom"
via: "useSearchParams"
pattern: "useSearchParams.*month"
- from: "src/components/dashboard/MonthNavigator.tsx"
to: "src/hooks/useMonthParam.ts"
via: "import"
pattern: "useMonthParam"
---
<objective>
Create the month navigation infrastructure and chart empty state component for the dashboard.
Purpose: Provides the URL-based month selection hook, the MonthNavigator UI (prev/next arrows + month dropdown), and a shared ChartEmptyState placeholder. These are foundational pieces consumed by all chart components and the dashboard layout in Plan 03.
Output: Three new files (useMonthParam hook, MonthNavigator component, ChartEmptyState component) plus updated i18n files with new translation keys.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-dashboard-charts-and-layout/02-CONTEXT.md
@.planning/phases/02-dashboard-charts-and-layout/02-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export interface Budget {
id: string; user_id: string; start_date: string; end_date: string;
currency: string; carryover_amount: number; created_at: string; updated_at: string;
}
```
From src/hooks/useBudgets.ts:
```typescript
export function useBudgets(): {
budgets: Budget[]; loading: boolean;
getBudget: (id: string) => ReturnType<typeof useBudgetDetail>;
createBudget: UseMutationResult; generateFromTemplate: UseMutationResult;
updateItem: UseMutationResult; createItem: UseMutationResult;
deleteItem: UseMutationResult; deleteBudget: UseMutationResult;
}
```
From src/components/shared/PageShell.tsx:
```typescript
interface PageShellProps {
title: string; description?: string; action?: React.ReactNode; children: React.ReactNode;
}
export function PageShell({ title, description, action, children }: PageShellProps): JSX.Element
```
From src/components/ui/chart.tsx:
```typescript
export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType } & ({ color?: string; theme?: never } | { color?: never; theme: Record<"light" | "dark", string> }) }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create useMonthParam hook and MonthNavigator component</name>
<files>src/hooks/useMonthParam.ts, src/components/dashboard/MonthNavigator.tsx</files>
<action>
**useMonthParam hook** (`src/hooks/useMonthParam.ts`):
- Import `useSearchParams` from `react-router-dom`
- Read `month` param from URL search params
- Fall back to current month as `YYYY-MM` format if param is missing
- Provide `setMonth(newMonth: string)` that updates the param using callback form: `setSearchParams(prev => { prev.set("month", value); return prev })` (preserves other params per Pitfall 5 from research)
- Provide `navigateMonth(delta: number)` that computes next/prev month using `new Date(year, mo - 1 + delta, 1)` for automatic year rollover
- Return `{ month, setMonth, navigateMonth }` where month is `YYYY-MM` string
- Export as named export `useMonthParam`
**MonthNavigator component** (`src/components/dashboard/MonthNavigator.tsx`):
- Accept props: `availableMonths: string[]` (array of `YYYY-MM` strings that have budgets), `t: (key: string) => string`
- Import `useMonthParam` from hooks
- Import `ChevronLeft`, `ChevronRight` from `lucide-react`
- Import `Button` from `@/components/ui/button`
- Import `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` from `@/components/ui/select`
- Layout: horizontal flex row with left arrow button, month selector (Select dropdown), right arrow button
- Left arrow: `Button` variant="ghost" size="icon" with `ChevronLeft`, onClick calls `navigateMonth(-1)`
- Right arrow: `Button` variant="ghost" size="icon" with `ChevronRight`, onClick calls `navigateMonth(1)`
- Center: `Select` component whose value is the current `month` from hook. `onValueChange` calls `setMonth`. SelectTrigger shows formatted month name (use `Date` to format `YYYY-MM` into locale-aware month+year display, e.g., "March 2026")
- SelectItems: map over `availableMonths` prop, displaying each as formatted month+year
- Arrow buttons allow navigating beyond existing budgets (per user decision) -- they just call navigateMonth regardless
- Dropdown only lists months that have budgets (per user decision)
- Keep presentational -- accept `t()` as prop (follows Phase 1 pattern)
- Export as named export `MonthNavigator`
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>useMonthParam hook reads/writes month URL param with fallback to current month. MonthNavigator renders prev/next arrows and a dropdown of available months. Build passes with no type errors.</done>
</task>
<task type="auto">
<name>Task 2: Create ChartEmptyState component and add i18n keys</name>
<files>src/components/dashboard/charts/ChartEmptyState.tsx, src/i18n/en.json, src/i18n/de.json</files>
<action>
**ChartEmptyState component** (`src/components/dashboard/charts/ChartEmptyState.tsx`):
- Create the `src/components/dashboard/charts/` directory
- Accept props: `message: string`, `className?: string`
- Render a muted placeholder inside a div matching chart area dimensions: `min-h-[250px] w-full` (matches ChartContainer sizing)
- Center content vertically and horizontally: `flex items-center justify-center`
- Background: `bg-muted/30 rounded-lg border border-dashed border-muted-foreground/20`
- Message text: `text-sm text-muted-foreground`
- This is a simple presentational component -- no chart logic, just the visual placeholder per user decision ("greyed-out chart outline with text overlay")
- Export as named export `ChartEmptyState`
**i18n keys** (add to both `en.json` and `de.json`):
Add new keys under the existing `"dashboard"` object. Do NOT remove any existing keys. Add:
```
"monthNav": "Month",
"noData": "No data to display",
"expenseDonut": "Expense Breakdown",
"incomeChart": "Income: Budget vs Actual",
"spendChart": "Spending by Category",
"budgeted": "Budgeted",
"actual": "Actual",
"noBudgetForMonth": "No budget for this month",
"createBudget": "Create Budget",
"generateFromTemplate": "Generate from Template"
```
German translations:
```
"monthNav": "Monat",
"noData": "Keine Daten vorhanden",
"expenseDonut": "Ausgabenverteilung",
"incomeChart": "Einkommen: Budget vs. Ist",
"spendChart": "Ausgaben nach Kategorie",
"budgeted": "Budgetiert",
"actual": "Tatsaechlich",
"noBudgetForMonth": "Kein Budget fuer diesen Monat",
"createBudget": "Budget erstellen",
"generateFromTemplate": "Aus Vorlage generieren"
```
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>ChartEmptyState renders a muted placeholder with centered message text. i18n files contain all new chart and month navigation keys in both English and German. Build passes.</done>
</task>
</tasks>
<verification>
- `bun run build` passes with no type errors
- `src/hooks/useMonthParam.ts` exports `useMonthParam` with `{ month, setMonth, navigateMonth }` return type
- `src/components/dashboard/MonthNavigator.tsx` exports `MonthNavigator` component
- `src/components/dashboard/charts/ChartEmptyState.tsx` exports `ChartEmptyState` component
- Both i18n files contain all new keys under `dashboard.*`
</verification>
<success_criteria>
- useMonthParam reads `?month=YYYY-MM` from URL, falls back to current month, provides setMonth and navigateMonth
- MonthNavigator shows prev/next arrows and a month dropdown
- ChartEmptyState renders a visually muted placeholder for empty charts
- All new i18n keys present in en.json and de.json
- `bun run build` passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-dashboard-charts-and-layout/02-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,263 @@
---
phase: 02-dashboard-charts-and-layout
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/dashboard/charts/ExpenseDonutChart.tsx
- src/components/dashboard/charts/IncomeBarChart.tsx
- src/components/dashboard/charts/SpendBarChart.tsx
autonomous: true
requirements: [UI-DONUT-01, UI-BAR-01, UI-HBAR-01]
must_haves:
truths:
- "Donut chart renders expense data by category type with center total label and active sector hover expansion"
- "Donut chart shows custom legend with category colors and formatted amounts"
- "Donut chart shows neutral empty ring with $0 center when all actuals are zero"
- "Vertical bar chart renders grouped budgeted vs actual bars for income with muted/vivid color distinction"
- "Horizontal bar chart renders budget vs actual spending by category type with over-budget red accent"
- "All three charts consume CSS variable tokens via ChartConfig -- no hardcoded hex values"
- "All three charts handle empty data by rendering ChartEmptyState placeholder"
artifacts:
- path: "src/components/dashboard/charts/ExpenseDonutChart.tsx"
provides: "Donut pie chart for expense breakdown"
exports: ["ExpenseDonutChart"]
min_lines: 60
- path: "src/components/dashboard/charts/IncomeBarChart.tsx"
provides: "Vertical grouped bar chart for income budget vs actual"
exports: ["IncomeBarChart"]
min_lines: 40
- path: "src/components/dashboard/charts/SpendBarChart.tsx"
provides: "Horizontal bar chart for category spend budget vs actual"
exports: ["SpendBarChart"]
min_lines: 40
key_links:
- from: "src/components/dashboard/charts/ExpenseDonutChart.tsx"
to: "@/components/ui/chart"
via: "ChartContainer + ChartConfig"
pattern: "ChartContainer.*config"
- from: "src/components/dashboard/charts/IncomeBarChart.tsx"
to: "@/components/ui/chart"
via: "ChartContainer + ChartConfig"
pattern: "ChartContainer.*config"
- from: "src/components/dashboard/charts/SpendBarChart.tsx"
to: "@/components/ui/chart"
via: "ChartContainer + ChartConfig"
pattern: "ChartContainer.*config"
- from: "src/components/dashboard/charts/ExpenseDonutChart.tsx"
to: "@/lib/format"
via: "formatCurrency for center label and legend"
pattern: "formatCurrency"
---
<objective>
Build the three chart components -- ExpenseDonutChart, IncomeBarChart, and SpendBarChart -- as isolated presentational components.
Purpose: These are the core visual deliverables of Phase 2. Each chart is self-contained, receives pre-computed data as props, uses ChartContainer/ChartConfig from shadcn for CSS-variable-driven color theming, and handles its own empty state. Plan 03 will wire them into the dashboard layout.
Output: Three chart component files in `src/components/dashboard/charts/`.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-dashboard-charts-and-layout/02-CONTEXT.md
@.planning/phases/02-dashboard-charts-and-layout/02-RESEARCH.md
<interfaces>
<!-- Key types, chart patterns, and color tokens the executor needs. -->
From src/components/ui/chart.tsx:
```typescript
export type ChartConfig = {
[k in string]: { label?: React.ReactNode; icon?: React.ComponentType } &
({ color?: string; theme?: never } | { color?: never; theme: Record<"light" | "dark", string> })
}
export function ChartContainer({ config, className, children, ...props }: { config: ChartConfig; children: ReactNode } & ComponentProps<"div">): JSX.Element
export const ChartTooltip: typeof RechartsPrimitive.Tooltip
export function ChartTooltipContent({ nameKey, ...props }: TooltipProps & { nameKey?: string }): JSX.Element
export const ChartLegend: typeof RechartsPrimitive.Legend
export function ChartLegendContent({ nameKey, payload, verticalAlign, ...props }: LegendProps & { nameKey?: string }): JSX.Element
```
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
```
From src/lib/palette.ts:
```typescript
export const categoryColors: Record<CategoryType, string>
// Values: "var(--color-income)", "var(--color-bill)", etc.
```
From src/lib/format.ts:
```typescript
export function formatCurrency(amount: number, currency?: string, locale?: string): string
```
CSS tokens available in index.css @theme:
```css
--color-income-fill: oklch(0.68 0.19 155);
--color-bill-fill: oklch(0.65 0.19 25);
--color-variable-expense-fill: oklch(0.70 0.18 50);
--color-debt-fill: oklch(0.60 0.20 355);
--color-saving-fill: oklch(0.68 0.18 220);
--color-investment-fill: oklch(0.65 0.18 285);
--color-over-budget: oklch(0.55 0.20 25);
--color-on-budget: oklch(0.50 0.17 155);
--color-budget-bar-bg: oklch(0.92 0.01 260);
```
From src/pages/DashboardPage.tsx (existing data patterns):
```typescript
const EXPENSE_TYPES: CategoryType[] = ["bill", "variable_expense", "debt", "saving", "investment"]
// pieData shape: { name: string, value: number, type: CategoryType }[]
// totalExpenses: number
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ExpenseDonutChart component</name>
<files>src/components/dashboard/charts/ExpenseDonutChart.tsx</files>
<action>
Create `src/components/dashboard/charts/ExpenseDonutChart.tsx`. If the `charts/` directory was not created by Plan 01 yet (parallel wave), create it.
**Props interface:**
```typescript
interface ExpenseDonutChartProps {
data: Array<{ type: string; value: number; label: string }>
totalExpenses: number
currency: string
emptyMessage: string // i18n-driven, passed from parent
}
```
**ChartConfig:** Build config from data entries, mapping each `type` to its fill color:
```typescript
const chartConfig = useMemo(() => {
const config: ChartConfig = {}
for (const entry of data) {
config[entry.type] = {
label: entry.label,
color: `var(--color-${entry.type}-fill)`,
}
}
return config
}, [data])
```
**Empty/Zero states:**
- If `data.length === 0` and `totalExpenses === 0`: check if this is a "no items at all" case. Render `ChartEmptyState` with `emptyMessage` prop. Import from `./ChartEmptyState`.
- If data is empty but there may be items with zero amounts: the parent will pass an `allZero` indicator (or the totalExpenses will be 0). When totalExpenses is 0 but the component is rendered, show a single neutral sector (full ring) in `var(--color-muted)` with `$0` center label. Use a synthetic data point: `[{ type: "empty", value: 1, label: "" }]` with `fill="var(--color-muted)"`.
**Donut rendering:**
- Wrap in `ChartContainer` with `config={chartConfig}` and `className="min-h-[250px] w-full"`
- Use `PieChart` > `Pie` with `dataKey="value"` `nameKey="type"` `innerRadius={60}` `outerRadius={85}` `cx="50%"` `cy="50%"`
- Active sector hover: maintain `activeIndex` state with `useState(-1)`. Set `activeShape` to a render function that draws a `Sector` with `outerRadius + 8` (expanded). Wire `onMouseEnter={(_, index) => setActiveIndex(index)}` and `onMouseLeave={() => setActiveIndex(-1)}` per Research Pattern 2.
- Cell coloring: map data entries to `<Cell key={entry.type} fill={`var(--color-${entry.type}-fill)`} />`
- Center label: use `<Label>` inside `<Pie>` with content function that checks `viewBox && "cx" in viewBox && "cy" in viewBox` (per Pitfall 4), then renders `<text>` with `textAnchor="middle"` `dominantBaseline="middle"` and a `<tspan className="fill-foreground text-xl font-bold">` showing `formatCurrency(totalExpenses, currency)`. Center label shows total expense amount only (per user decision -- no label text).
**Custom legend:** Below the chart, render a `<ul>` with legend items (following the existing pattern from DashboardPage.tsx lines 168-182). Each item shows a color dot (using `var(--color-${entry.type}-fill)` as background), the label text, and the formatted amount right-aligned. Use the shadcn `ChartLegend` + `ChartLegendContent` if it works well with the pie chart nameKey, otherwise use the manual ul-based legend matching the existing codebase pattern. Place legend below the donut (per research recommendation for tight 3-column layout).
**Tooltip:** Use `<ChartTooltip content={<ChartTooltipContent nameKey="type" formatter={(value) => formatCurrency(Number(value), currency)} />} />` for formatted currency tooltips.
**Import order:** Follow conventions -- React first, then recharts, then @/ imports.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>ExpenseDonutChart renders a donut with center total label, active sector expansion on hover, custom legend below, CSS variable fills, and handles empty/zero-amount states. Build passes.</done>
</task>
<task type="auto">
<name>Task 2: Create IncomeBarChart and SpendBarChart components</name>
<files>src/components/dashboard/charts/IncomeBarChart.tsx, src/components/dashboard/charts/SpendBarChart.tsx</files>
<action>
**IncomeBarChart** (`src/components/dashboard/charts/IncomeBarChart.tsx`):
Props interface:
```typescript
interface IncomeBarChartProps {
data: Array<{ label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
```
- If `data.length === 0`, render `ChartEmptyState` with `emptyMessage`
- ChartConfig: `{ budgeted: { label: "Budgeted" (from props or hardcode), color: "var(--color-budget-bar-bg)" }, actual: { label: "Actual", color: "var(--color-income-fill)" } } satisfies ChartConfig`
- Wrap in `ChartContainer` with `className="min-h-[250px] w-full"`
- Use `BarChart` (vertical, which is the default -- no `layout` prop needed)
- `<CartesianGrid vertical={false} />` for horizontal grid lines only
- `<XAxis dataKey="label" tick={{ fontSize: 12 }} />` for category labels
- `<YAxis tick={{ fontSize: 12 }} />` for amount axis
- Two `<Bar>` components (NOT stacked -- no `stackId`): `<Bar dataKey="budgeted" fill="var(--color-budgeted)" radius={[4, 4, 0, 0]} />` and `<Bar dataKey="actual" radius={[4, 4, 0, 0]}>` with `<Cell>` per entry: if `entry.actual > entry.budgeted`, use `fill="var(--color-over-budget)"`, otherwise use `fill="var(--color-income-fill)"` (per user decision: actual bars vivid, over-budget bars red)
- Tooltip: `<ChartTooltip content={<ChartTooltipContent formatter={(value) => formatCurrency(Number(value), currency)} />} />`
- ChartLegend: `<ChartLegend content={<ChartLegendContent />} />` for budgeted/actual legend
**SpendBarChart** (`src/components/dashboard/charts/SpendBarChart.tsx`):
Props interface:
```typescript
interface SpendBarChartProps {
data: Array<{ type: string; label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
```
- If `data.length === 0`, render `ChartEmptyState` with `emptyMessage`
- ChartConfig: `{ budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" }, actual: { label: "Actual", color: "var(--color-muted-foreground)" } } satisfies ChartConfig` (base color for actual; overridden per-cell)
- Wrap in `ChartContainer` with `className="min-h-[250px] w-full"`
- **CRITICAL: Horizontal bars via `layout="vertical"`** on `<BarChart>` (per Research Pattern 3 and Pitfall 2)
- `<CartesianGrid horizontal={false} />` -- only vertical grid lines for horizontal bar layout
- `<XAxis type="number" hide />` (number axis, hidden)
- `<YAxis type="category" dataKey="label" width={120} tick={{ fontSize: 12 }} />` (category labels on Y axis)
- Two `<Bar>` components (NOT stacked): `<Bar dataKey="budgeted" fill="var(--color-budgeted)" radius={4} />` and `<Bar dataKey="actual" radius={4}>` with `<Cell>` per entry: if `entry.actual > entry.budgeted`, fill is `"var(--color-over-budget)"` (red accent per user decision), otherwise fill is `var(--color-${entry.type}-fill)` (vivid category color)
- Tooltip and Legend same pattern as IncomeBarChart
- The actual bar naturally extending past the budgeted bar IS the over-budget visual indicator (per Research Pattern 5)
**Both components:** Follow project import conventions. Use named exports. Accept `t()` translations via props or use i18n keys in config labels.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>IncomeBarChart renders grouped vertical bars (budgeted muted, actual vivid) with over-budget red fill. SpendBarChart renders horizontal bars via layout="vertical" with per-cell over-budget coloring. Both handle empty data with ChartEmptyState. Build passes.</done>
</task>
</tasks>
<verification>
- `bun run build` passes with no type errors
- All three chart files exist in `src/components/dashboard/charts/`
- Each chart uses `ChartContainer` as its outer wrapper (not raw `ResponsiveContainer`)
- No hardcoded hex color values -- all colors via CSS variables
- Each chart handles empty data gracefully (ChartEmptyState or neutral ring)
- ExpenseDonutChart has center label with formatted currency, active hover, and legend
- IncomeBarChart has two grouped (not stacked) bars
- SpendBarChart uses `layout="vertical"` with swapped axis types
</verification>
<success_criteria>
- ExpenseDonutChart renders donut with center total, hover expansion, and custom legend using CSS variable fills
- IncomeBarChart renders grouped vertical bars comparing budgeted (muted) vs actual (vivid) for income
- SpendBarChart renders horizontal bars comparing budget vs actual by category type with over-budget red accent
- All charts handle zero-data and empty states
- `bun run build` passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-dashboard-charts-and-layout/02-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,282 @@
---
phase: 02-dashboard-charts-and-layout
plan: 03
type: execute
wave: 2
depends_on: ["02-01", "02-02"]
files_modified:
- src/pages/DashboardPage.tsx
- src/components/dashboard/DashboardSkeleton.tsx
autonomous: true
requirements: [UI-DASH-01, UI-BAR-01, UI-HBAR-01, UI-DONUT-01]
must_haves:
truths:
- "Dashboard page reads month from URL search params and looks up the corresponding budget"
- "MonthNavigator appears in the PageShell action slot with a dropdown of all available budget months"
- "Dashboard displays SummaryStrip, then a 3-column chart row (donut, vertical bar, horizontal bar), then QuickAdd button"
- "Charts and cards update when user navigates to a different month"
- "When no budget exists for the selected month, an empty prompt is shown with create/generate options"
- "DashboardSkeleton mirrors the new 3-column chart layout"
artifacts:
- path: "src/pages/DashboardPage.tsx"
provides: "Refactored dashboard with URL month nav and 3-column chart grid"
exports: ["default"]
min_lines: 80
- path: "src/components/dashboard/DashboardSkeleton.tsx"
provides: "Updated skeleton matching 3-column chart layout"
exports: ["DashboardSkeleton"]
key_links:
- from: "src/pages/DashboardPage.tsx"
to: "src/hooks/useMonthParam.ts"
via: "useMonthParam hook for month state"
pattern: "useMonthParam"
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/MonthNavigator.tsx"
via: "MonthNavigator in PageShell action slot"
pattern: "MonthNavigator"
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/charts/ExpenseDonutChart.tsx"
via: "import and render in chart grid"
pattern: "ExpenseDonutChart"
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/charts/IncomeBarChart.tsx"
via: "import and render in chart grid"
pattern: "IncomeBarChart"
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/charts/SpendBarChart.tsx"
via: "import and render in chart grid"
pattern: "SpendBarChart"
- from: "src/pages/DashboardPage.tsx"
to: "src/hooks/useBudgets.ts"
via: "useBudgets for budget list + useBudgetDetail for selected budget"
pattern: "useBudgets.*useBudgetDetail"
---
<objective>
Wire all chart components, month navigation, and updated layout into the DashboardPage, and update the DashboardSkeleton to match.
Purpose: This is the integration plan that ties together the month navigation (Plan 01) and chart components (Plan 02) into the refactored dashboard. Replaces the existing flat pie chart and progress bars with the 3-column chart grid, adds URL-based month navigation, and updates the loading skeleton.
Output: Refactored DashboardPage.tsx and updated DashboardSkeleton.tsx.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-dashboard-charts-and-layout/02-CONTEXT.md
@.planning/phases/02-dashboard-charts-and-layout/02-RESEARCH.md
@.planning/phases/02-dashboard-charts-and-layout/02-01-SUMMARY.md
@.planning/phases/02-dashboard-charts-and-layout/02-02-SUMMARY.md
<interfaces>
<!-- Contracts from Plan 01 and Plan 02 that this plan consumes. -->
From src/hooks/useMonthParam.ts (Plan 01):
```typescript
export function useMonthParam(): {
month: string // "YYYY-MM"
setMonth: (newMonth: string) => void
navigateMonth: (delta: number) => void
}
```
From src/components/dashboard/MonthNavigator.tsx (Plan 01):
```typescript
interface MonthNavigatorProps {
availableMonths: string[] // "YYYY-MM"[]
t: (key: string) => string
}
export function MonthNavigator({ availableMonths, t }: MonthNavigatorProps): JSX.Element
```
From src/components/dashboard/charts/ChartEmptyState.tsx (Plan 01):
```typescript
export function ChartEmptyState({ message, className }: { message: string; className?: string }): JSX.Element
```
From src/components/dashboard/charts/ExpenseDonutChart.tsx (Plan 02):
```typescript
interface ExpenseDonutChartProps {
data: Array<{ type: string; value: number; label: string }>
totalExpenses: number
currency: string
emptyMessage: string
}
export function ExpenseDonutChart(props: ExpenseDonutChartProps): JSX.Element
```
From src/components/dashboard/charts/IncomeBarChart.tsx (Plan 02):
```typescript
interface IncomeBarChartProps {
data: Array<{ label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
export function IncomeBarChart(props: IncomeBarChartProps): JSX.Element
```
From src/components/dashboard/charts/SpendBarChart.tsx (Plan 02):
```typescript
interface SpendBarChartProps {
data: Array<{ type: string; label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
export function SpendBarChart(props: SpendBarChartProps): JSX.Element
```
From src/components/shared/PageShell.tsx:
```typescript
export function PageShell({ title, description, action, children }: PageShellProps): JSX.Element
// action slot renders top-right, ideal for MonthNavigator
```
From src/hooks/useBudgets.ts:
```typescript
export function useBudgets(): { budgets: Budget[]; loading: boolean; createBudget: UseMutationResult; generateFromTemplate: UseMutationResult; ... }
export function useBudgetDetail(id: string): { budget: Budget | null; items: BudgetItem[]; loading: boolean }
```
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export interface Budget { id: string; start_date: string; end_date: string; currency: string; carryover_amount: number; ... }
export interface BudgetItem { id: string; budget_id: string; category_id: string; budgeted_amount: number; actual_amount: number; category?: Category; ... }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Refactor DashboardPage with month navigation and 3-column chart grid</name>
<files>src/pages/DashboardPage.tsx</files>
<action>
Rewrite `src/pages/DashboardPage.tsx` to replace the existing flat pie chart + progress bars with the new 3-chart layout and URL-based month navigation.
**DashboardPage (outer component):**
- Remove the hardcoded `currentMonthStart` helper and the `now`/`year`/`month`/`monthPrefix` date logic
- Import `useMonthParam` from `@/hooks/useMonthParam`
- Import `MonthNavigator` from `@/components/dashboard/MonthNavigator`
- Call `const { month } = useMonthParam()` to get the selected month as `YYYY-MM`
- Call `const { budgets, loading } = useBudgets()`
- Derive `availableMonths` from budgets: `useMemo(() => budgets.map(b => b.start_date.slice(0, 7)), [budgets])` -- array of `YYYY-MM` strings
- Find current budget: `useMemo(() => budgets.find(b => b.start_date.startsWith(month)), [budgets, month])` -- uses `startsWith` prefix matching (per Pitfall 7)
- Pass `MonthNavigator` into PageShell `action` slot: `<PageShell title={t("dashboard.title")} action={<MonthNavigator availableMonths={availableMonths} t={t} />}>`
- Loading state: show `DashboardSkeleton` inside PageShell (same as current)
- No budget for month: show empty prompt with `t("dashboard.noBudgetForMonth")` text and two buttons:
- "Create Budget" button calling `createBudget.mutate({ month: parsedMonth, year: parsedYear, currency: "EUR" })`
- "Generate from Template" button calling `generateFromTemplate.mutate({ month: parsedMonth, year: parsedYear, currency: "EUR" })`
- Parse month/year from the `month` string (split on "-")
- Per user decision: empty prompt when navigating to month with no budget, with create/generate option
- When budget exists: render `<DashboardContent budgetId={currentBudget.id} />`
**DashboardContent (inner component):**
- Keep `useBudgetDetail(budgetId)` call and the loading/null guards
- Keep existing derived totals logic (totalIncome, totalExpenses, availableBalance, budgetedIncome, budgetedExpenses)
- Memoize all derived data with `useMemo` (wrap existing reduce operations)
- **Derive pieData** (same as existing but memoized):
```typescript
const pieData = useMemo(() =>
EXPENSE_TYPES.map(type => {
const total = items.filter(i => i.category?.type === type).reduce((sum, i) => sum + i.actual_amount, 0)
return { type, value: total, label: t(`categories.types.${type}`) }
}).filter(d => d.value > 0),
[items, t])
```
- **Derive incomeBarData** (NEW):
```typescript
const incomeBarData = useMemo(() => {
const budgeted = items.filter(i => i.category?.type === "income").reduce((sum, i) => sum + i.budgeted_amount, 0)
const actual = items.filter(i => i.category?.type === "income").reduce((sum, i) => sum + i.actual_amount, 0)
if (budgeted === 0 && actual === 0) return []
return [{ label: t("categories.types.income"), budgeted, actual }]
}, [items, t])
```
- **Derive spendBarData** (NEW):
```typescript
const spendBarData = useMemo(() =>
EXPENSE_TYPES.map(type => {
const groupItems = items.filter(i => i.category?.type === type)
if (groupItems.length === 0) return null
const budgeted = groupItems.reduce((sum, i) => sum + i.budgeted_amount, 0)
const actual = groupItems.reduce((sum, i) => sum + i.actual_amount, 0)
return { type, label: t(`categories.types.${type}`), budgeted, actual }
}).filter(Boolean) as Array<{ type: string; label: string; budgeted: number; actual: number }>,
[items, t])
```
- **Layout (JSX):** Replace the entire existing chart/progress section with:
1. `SummaryStrip` (same as current -- first row)
2. Chart row: `<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">` containing three `Card` wrappers:
- Card 1: `<Card><CardHeader><CardTitle className="text-base">{t("dashboard.expenseDonut")}</CardTitle></CardHeader><CardContent><ExpenseDonutChart data={pieData} totalExpenses={totalExpenses} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card>`
- Card 2: `<Card><CardHeader><CardTitle className="text-base">{t("dashboard.incomeChart")}</CardTitle></CardHeader><CardContent><IncomeBarChart data={incomeBarData} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card>`
- Card 3: `<Card><CardHeader><CardTitle className="text-base">{t("dashboard.spendChart")}</CardTitle></CardHeader><CardContent><SpendBarChart data={spendBarData} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card>`
3. `QuickAddPicker` row (moved below charts per user decision)
- **Remove:** The old `PieChart`, `Pie`, `Cell`, `ResponsiveContainer`, `Tooltip` imports from recharts (replaced by chart components). Remove the old `progressGroups` derivation. Remove the old 2-column grid layout. Remove old inline pie chart and progress bar JSX.
- **Keep:** EXPENSE_TYPES constant (still used for data derivation), all SummaryStrip logic, QuickAddPicker integration.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>DashboardPage uses URL search params for month selection, MonthNavigator in PageShell action slot, and a 3-column chart grid (donut, vertical bar, horizontal bar) replacing the old pie chart + progress bars. Empty month prompt shows create/generate buttons. Build passes.</done>
</task>
<task type="auto">
<name>Task 2: Update DashboardSkeleton for 3-column chart layout</name>
<files>src/components/dashboard/DashboardSkeleton.tsx</files>
<action>
Update `src/components/dashboard/DashboardSkeleton.tsx` to mirror the new dashboard layout. The skeleton must match the real layout structure to prevent layout shift on load (established pattern from Phase 1).
**Changes:**
- Keep the 3-card summary skeleton row unchanged: `<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">` with 3 `SkeletonStatCard` components
- Replace the 2-column chart skeleton with a 3-column grid: `<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">`
- Each chart skeleton card: `<Card><CardHeader><Skeleton className="h-5 w-40" /></CardHeader><CardContent><Skeleton className="h-[250px] w-full rounded-md" /></CardContent></Card>`
- Three skeleton chart cards (matching the donut, bar, bar layout)
- Keep the `SkeletonStatCard` helper component unchanged
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>DashboardSkeleton mirrors the new 3-column chart layout with 3 skeleton chart cards. Build passes.</done>
</task>
</tasks>
<verification>
- `bun run build` passes with no type errors
- `bun run lint` passes (or pre-existing errors only)
- DashboardPage imports and renders all 3 chart components
- DashboardPage uses `useMonthParam` for month state (no `useState` for month)
- MonthNavigator placed in PageShell `action` slot
- No old recharts direct imports remain in DashboardPage (PieChart, Pie, Cell, ResponsiveContainer, Tooltip)
- No old progress bar JSX remains
- Chart grid uses `lg:grid-cols-3` responsive breakpoint
- DashboardSkeleton has 3 chart skeleton cards matching real layout
</verification>
<success_criteria>
- User can navigate between months using prev/next arrows and month dropdown
- Month is stored in URL search params (`?month=YYYY-MM`)
- Dashboard shows SummaryStrip, then 3-column chart row, then QuickAdd
- Charts and summary cards update when month changes
- Empty month shows create/generate prompt
- DashboardSkeleton mirrors new layout
- `bun run build && bun run lint` passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-dashboard-charts-and-layout/02-03-SUMMARY.md`
</output>