docs(02): create phase plan — 3 plans across 2 waves for dashboard charts and layout
This commit is contained in:
@@ -37,21 +37,22 @@ Plans:
|
|||||||
- [x] 01-02-PLAN.md — Build PageShell, StatCard, SummaryStrip, DashboardSkeleton and integrate into DashboardPage
|
- [x] 01-02-PLAN.md — Build PageShell, StatCard, SummaryStrip, DashboardSkeleton and integrate into DashboardPage
|
||||||
|
|
||||||
### Phase 2: Dashboard Charts and Layout
|
### 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
|
**Depends on**: Phase 1
|
||||||
**Requirements**: UI-DASH-01, UI-BAR-01, UI-HBAR-01, UI-DONUT-01
|
**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):
|
**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
|
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
|
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
|
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)
|
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
|
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:
|
Plans:
|
||||||
- [ ] 02-01: TBD
|
- [ ] 02-01-PLAN.md — Month navigation infrastructure (useMonthParam hook, MonthNavigator, ChartEmptyState, i18n keys)
|
||||||
- [ ] 02-02: TBD
|
- [ ] 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
|
### 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
|
**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 |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Design Foundation and Primitives | 2/2 | Complete | 2026-03-16 |
|
| 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 | - |
|
| 3. Collapsible Dashboard Sections | 0/TBD | Not started | - |
|
||||||
| 4. Full-App Design Consistency | 0/TBD | Not started | - |
|
| 4. Full-App Design Consistency | 0/TBD | Not started | - |
|
||||||
|
|||||||
207
.planning/phases/02-dashboard-charts-and-layout/02-01-PLAN.md
Normal file
207
.planning/phases/02-dashboard-charts-and-layout/02-01-PLAN.md
Normal 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>
|
||||||
263
.planning/phases/02-dashboard-charts-and-layout/02-02-PLAN.md
Normal file
263
.planning/phases/02-dashboard-charts-and-layout/02-02-PLAN.md
Normal 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>
|
||||||
282
.planning/phases/02-dashboard-charts-and-layout/02-03-PLAN.md
Normal file
282
.planning/phases/02-dashboard-charts-and-layout/02-03-PLAN.md
Normal 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>
|
||||||
Reference in New Issue
Block a user