diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 257ec37..d8fcf12 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 | - | diff --git a/.planning/phases/02-dashboard-charts-and-layout/02-01-PLAN.md b/.planning/phases/02-dashboard-charts-and-layout/02-01-PLAN.md new file mode 100644 index 0000000..45a5555 --- /dev/null +++ b/.planning/phases/02-dashboard-charts-and-layout/02-01-PLAN.md @@ -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" +--- + + +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. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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; + 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> }) } +``` + + + + + + + Task 1: Create useMonthParam hook and MonthNavigator component + src/hooks/useMonthParam.ts, src/components/dashboard/MonthNavigator.tsx + +**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` + + + cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build + + 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. + + + + Task 2: Create ChartEmptyState component and add i18n keys + src/components/dashboard/charts/ChartEmptyState.tsx, src/i18n/en.json, src/i18n/de.json + +**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" +``` + + + cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build + + 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. + + + + + +- `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.*` + + + +- 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 + + + +After completion, create `.planning/phases/02-dashboard-charts-and-layout/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-dashboard-charts-and-layout/02-02-PLAN.md b/.planning/phases/02-dashboard-charts-and-layout/02-02-PLAN.md new file mode 100644 index 0000000..e61df42 --- /dev/null +++ b/.planning/phases/02-dashboard-charts-and-layout/02-02-PLAN.md @@ -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" +--- + + +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/`. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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 +// 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 +``` + + + + + + + Task 1: Create ExpenseDonutChart component + src/components/dashboard/charts/ExpenseDonutChart.tsx + +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 `` +- Center label: use `