` wrapper for spacing
+- Table with 4 columns per user decision: Item Name, Budgeted, Actual, Difference
+- TableHeader: use `t("dashboard.sections.itemName")`, `t("budgets.budgeted")`, `t("budgets.actual")`, `t("budgets.difference")` — last 3 columns right-aligned
+- TableBody: map items, each row:
+ - Name cell: `item.category?.name ?? item.category_id`, `className="font-medium"`
+ - Budgeted cell: `formatCurrency(item.budgeted_amount, currency)`, `className="text-right tabular-nums"`
+ - Actual cell: `formatCurrency(item.actual_amount, currency)`, `className="text-right tabular-nums"`
+ - Difference cell: direction-aware diff calculation (same isIncome logic as header), color-coded, `className="text-right tabular-nums"` with `text-over-budget` when item is over, else `text-muted-foreground`
+- TableFooter: bold group totals row
+ - First cell: `t("dashboard.sections.groupTotal", { label })`, `className="font-medium"`
+ - Three total cells: budgeted, actual, diff — all `font-medium text-right tabular-nums`
+ - Footer diff uses same color coding as header (isOver ? text-over-budget : text-on-budget)
+
+**Per-item difference direction awareness:**
+- Each item's direction depends on its category type (which is `type` for all items in this section since they're pre-grouped)
+- For spending types: item diff = `item.budgeted_amount - item.actual_amount`, isOver = `item.actual_amount > item.budgeted_amount`
+- For income/saving/investment: item diff = `item.actual_amount - item.budgeted_amount`, isOver = `item.actual_amount < item.budgeted_amount`
+
+**2. Create src/components/dashboard/CollapsibleSections.tsx:**
+
+Container component that renders an ordered list of CategorySection components.
+
+```typescript
+interface GroupData {
+ type: CategoryType
+ label: string
+ items: BudgetItem[]
+ budgeted: number
+ actual: number
+}
+
+interface CollapsibleSectionsProps {
+ groups: GroupData[]
+ currency: string
+ openSections: Record
+ onToggleSection: (type: string, open: boolean) => void
+ t: (key: string, opts?: Record) => string
+}
+```
+
+Implementation:
+- Import `CategorySection` from `./CategorySection`
+- Import `CategoryType`, `BudgetItem` from `@/lib/types`
+- Render a `` wrapping `groups.map(...)`
+- For each group, render `
onToggleSection(group.type, open)} t={t} />`
+
+This component is thin glue — its purpose is to keep DashboardContent clean and provide a clear interface boundary for Plan 02 to wire into.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build
+
+
+- CategorySection.tsx exports a presentational collapsible section with header badges, chevron rotation, line-item table with 4 columns, and footer totals
+- Direction-aware difference logic implemented per user decision (spending: over when actual > budget; income/saving/investment: over when actual < budget)
+- CollapsibleSections.tsx exports a container that renders ordered CategorySection list with controlled open state
+- Both components accept t() as prop (presentational pattern)
+- Lint and build pass
+
+
+
+
+
+
+- `bun run lint` passes with no new errors
+- `bun run build` succeeds (TypeScript compile + Vite bundle)
+- StatCard renders subtitle when provided, hides when undefined
+- CSS animation keyframes defined for collapsible-open and collapsible-close
+- i18n keys present in both en.json and de.json
+- CategorySection and CollapsibleSections importable from components/dashboard/
+
+
+
+- Carryover subtitle flows from DashboardContent through SummaryStrip to StatCard balance card
+- CategorySection renders correct header layout: left border accent, chevron, label, badges, difference
+- CategorySection renders correct table: 4 columns, direction-aware coloring, footer totals
+- CollapsibleSections renders all groups with controlled open state
+- No TypeScript errors, no lint errors, build succeeds
+
+
+
diff --git a/.planning/phases/03-collapsible-dashboard-sections/03-02-PLAN.md b/.planning/phases/03-collapsible-dashboard-sections/03-02-PLAN.md
new file mode 100644
index 0000000..06ae082
--- /dev/null
+++ b/.planning/phases/03-collapsible-dashboard-sections/03-02-PLAN.md
@@ -0,0 +1,351 @@
+---
+phase: 03-collapsible-dashboard-sections
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 03-01
+files_modified:
+ - src/pages/DashboardPage.tsx
+ - src/components/dashboard/DashboardSkeleton.tsx
+autonomous: false
+requirements:
+ - UI-DASH-01
+ - UI-COLLAPSE-01
+
+must_haves:
+ truths:
+ - "Each non-empty category group renders as a collapsible section between charts and QuickAdd"
+ - "Over-budget sections auto-expand on load (direction-aware: spending overspent, income/savings under-earned/saved)"
+ - "On/under-budget sections start collapsed"
+ - "Empty category groups are hidden entirely"
+ - "Expand/collapse state resets when navigating months"
+ - "Toggling sections does not produce ResizeObserver loop errors or chart resize jank"
+ - "Collapsible sections animate open/close smoothly with no flicker on mount"
+ - "DashboardSkeleton mirrors the sections area layout"
+ artifacts:
+ - path: "src/pages/DashboardPage.tsx"
+ provides: "groupedSections useMemo, openSections state, CollapsibleSections rendering"
+ contains: "groupedSections"
+ - path: "src/components/dashboard/DashboardSkeleton.tsx"
+ provides: "Skeleton placeholders for collapsible sections area"
+ contains: "Skeleton"
+ key_links:
+ - from: "src/pages/DashboardPage.tsx"
+ to: "src/components/dashboard/CollapsibleSections.tsx"
+ via: "renders CollapsibleSections with grouped data and open state"
+ pattern: "CollapsibleSections.*groups.*openSections"
+ - from: "src/pages/DashboardPage.tsx"
+ to: "useBudgetDetail items"
+ via: "groupedSections useMemo derives groups from items"
+ pattern: "groupedSections.*useMemo.*items\\.filter"
+---
+
+
+Wire the collapsible sections into DashboardContent with smart expand/collapse defaults, month-navigation state reset, and chart isolation. Update DashboardSkeleton.
+
+Purpose: Complete the dashboard hybrid view by integrating the CategorySection/CollapsibleSections components built in Plan 01 into the live dashboard page with all the required state management.
+Output: Fully functional collapsible sections on the dashboard, DashboardSkeleton updated.
+
+
+
+@/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/03-collapsible-dashboard-sections/03-CONTEXT.md
+@.planning/phases/03-collapsible-dashboard-sections/03-RESEARCH.md
+@.planning/phases/03-collapsible-dashboard-sections/03-VALIDATION.md
+@.planning/phases/03-collapsible-dashboard-sections/03-01-SUMMARY.md
+
+@src/pages/DashboardPage.tsx
+@src/components/dashboard/DashboardSkeleton.tsx
+@src/components/dashboard/CollapsibleSections.tsx
+@src/components/dashboard/CategorySection.tsx
+
+
+
+
+From src/components/dashboard/CollapsibleSections.tsx (created in Plan 01):
+```typescript
+interface GroupData {
+ type: CategoryType
+ label: string
+ items: BudgetItem[]
+ budgeted: number
+ actual: number
+}
+
+interface CollapsibleSectionsProps {
+ groups: GroupData[]
+ currency: string
+ openSections: Record
+ onToggleSection: (type: string, open: boolean) => void
+ t: (key: string, opts?: Record) => string
+}
+
+export function CollapsibleSections(props: CollapsibleSectionsProps): JSX.Element
+```
+
+From src/pages/DashboardPage.tsx (current state after Plan 01):
+```typescript
+// DashboardContent receives { budgetId: string }
+// Uses useBudgetDetail(budgetId) -> { budget, items, loading }
+// Already has: totalIncome, totalExpenses, budgetedIncome, budgetedExpenses, pieData, incomeBarData, spendBarData useMemos
+// Already has: carryover subtitle computation from Plan 01
+// Layout: SummaryStrip -> chart grid -> QuickAdd
+// Collapsible sections insert between chart grid and QuickAdd
+```
+
+From src/lib/types.ts:
+```typescript
+export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
+export interface BudgetItem { id: string; budgeted_amount: number; actual_amount: number; category?: Category }
+```
+
+
+
+
+
+
+ Task 1: Wire collapsible sections into DashboardContent with smart defaults
+ src/pages/DashboardPage.tsx, src/components/dashboard/DashboardSkeleton.tsx
+
+**1. Add imports to DashboardPage.tsx:**
+
+```typescript
+import { useState, useMemo, useEffect } from "react" // add useState, useEffect
+import { CollapsibleSections } from "@/components/dashboard/CollapsibleSections"
+```
+
+**2. Add CATEGORY_TYPES_ALL constant (near top, alongside existing EXPENSE_TYPES):**
+
+```typescript
+const CATEGORY_TYPES_ALL: CategoryType[] = [
+ "income",
+ "bill",
+ "variable_expense",
+ "debt",
+ "saving",
+ "investment",
+]
+```
+
+**3. Add isOverBudget helper function at module level (near constants):**
+
+```typescript
+function isOverBudget(type: CategoryType, budgeted: number, actual: number): boolean {
+ if (type === "income" || type === "saving" || type === "investment") {
+ return actual < budgeted // under-earned / under-saved
+ }
+ return actual > budgeted // overspent
+}
+```
+
+**4. Add groupedSections useMemo in DashboardContent:**
+
+Place after the existing `spendBarData` useMemo and BEFORE the early returns (`if (loading)` / `if (!budget)`). This follows the established hooks-before-returns pattern from Phase 2.
+
+```typescript
+const groupedSections = useMemo(() =>
+ CATEGORY_TYPES_ALL
+ .map((type) => {
+ const groupItems = items.filter((i) => i.category?.type === type)
+ if (groupItems.length === 0) return null
+ const budgeted = groupItems.reduce((s, i) => s + i.budgeted_amount, 0)
+ const actual = groupItems.reduce((s, i) => s + i.actual_amount, 0)
+ return {
+ type,
+ label: t(`categories.types.${type}`),
+ items: groupItems,
+ budgeted,
+ actual,
+ }
+ })
+ .filter((g): g is NonNullable => g !== null),
+ [items, t]
+)
+```
+
+**5. Add openSections state and reset effect (after groupedSections, before early returns):**
+
+```typescript
+const [openSections, setOpenSections] = useState>(() =>
+ Object.fromEntries(
+ groupedSections.map((g) => [g.type, isOverBudget(g.type, g.budgeted, g.actual)])
+ )
+)
+
+// Reset expand state when month (budgetId) changes
+useEffect(() => {
+ setOpenSections(
+ Object.fromEntries(
+ groupedSections.map((g) => [g.type, isOverBudget(g.type, g.budgeted, g.actual)])
+ )
+ )
+}, [budgetId]) // budgetId changes on month navigation; groupedSections flows from it
+```
+
+IMPORTANT: `useState` and `useEffect` must be called before any early returns (hooks rules). The ordering in DashboardContent should be:
+1. All existing useMemo hooks (totalIncome, etc.)
+2. groupedSections useMemo (new)
+3. openSections useState (new)
+4. openSections useEffect (new)
+5. Early returns (loading, !budget)
+6. Computed values and JSX
+
+**6. Add handleToggleSection callback (after early returns, before JSX return):**
+
+```typescript
+const handleToggleSection = (type: string, open: boolean) => {
+ setOpenSections((prev) => ({ ...prev, [type]: open }))
+}
+```
+
+**7. Update the JSX layout in DashboardContent:**
+
+Insert `CollapsibleSections` between the chart grid ` ` and the QuickAdd ``:
+
+```tsx
+{/* Collapsible category sections */}
+{groupedSections.length > 0 && (
+
+)}
+```
+
+The final DashboardContent JSX order becomes:
+1. SummaryStrip
+2. Chart grid (3-column)
+3. CollapsibleSections (new)
+4. QuickAdd button
+
+**8. Update DashboardSkeleton (src/components/dashboard/DashboardSkeleton.tsx):**
+
+Add skeleton placeholders for the collapsible sections area. After the chart grid skeleton div, add:
+
+```tsx
+{/* Collapsible sections skeleton */}
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+```
+
+This mirrors 3 collapsed section headers (the most common default state), matching the real CategorySection header structure.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build
+
+
+- DashboardContent derives groupedSections from items via useMemo (filters empty groups, computes totals)
+- openSections state initializes with direction-aware smart defaults (over-budget expanded, others collapsed)
+- openSections resets via useEffect keyed on budgetId (month navigation)
+- CollapsibleSections renders between chart grid and QuickAdd
+- All hooks declared before early returns (Rules of Hooks compliance)
+- DashboardSkeleton includes section header placeholders
+- Lint and build pass
+
+
+
+
+ Task 2: Verify collapsible sections and carryover display
+ none
+
+Human verification of the complete Phase 3 feature set. No code changes — this is a visual/functional verification checkpoint.
+
+**What was built across Plan 01 and Plan 02:**
+- Collapsible per-category sections between charts and QuickAdd
+- Smart expand/collapse defaults (over-budget sections auto-expand)
+- Carryover subtitle on balance card when non-zero
+- Smooth expand/collapse animation
+- Direction-aware difference coloring
+- DashboardSkeleton updated with section placeholders
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ Human approves all 11 verification checks pass
+
+Complete dashboard hybrid view with:
+- Collapsible per-category sections between charts and QuickAdd
+- Smart expand/collapse defaults (over-budget sections auto-expand)
+- Carryover subtitle on balance card when non-zero
+- Smooth expand/collapse animation
+- Direction-aware difference coloring
+
+
+1. Run `cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run dev` and open http://localhost:5173
+
+2. **Collapsible sections visible:** Navigate to a month with budget items. Verify collapsible sections appear below the chart grid and above the QuickAdd button.
+
+3. **Section header design:** Each section should have:
+ - Thick colored left border (category color)
+ - Chevron icon on the left
+ - Category group label (e.g., "Income", "Bills")
+ - Two badges on the right: "Budgeted $X" and "Actual $X"
+ - Color-coded difference (green for on-budget, red for over-budget)
+
+4. **Smart defaults:** If any category group is over-budget (e.g., spending actual > budget), that section should be expanded on page load. On-budget sections should be collapsed.
+
+5. **Expand/collapse animation:** Click a section header. It should expand with a smooth 200ms animation. Click again to collapse. No layout jank in the charts above.
+
+6. **Line-item table:** Expanded sections show a 4-column table: Item Name, Budgeted, Actual, Difference. Footer row with bold group totals.
+
+7. **Empty groups hidden:** If a category type has zero budget items, it should not appear at all.
+
+8. **Month navigation reset:** Expand/collapse some sections, then navigate to a different month. Smart defaults should recalculate.
+
+9. **Carryover display:** If the budget has a non-zero `carryover_amount`, the balance card should show "Includes $X carryover" in small text below the balance value. If carryover is zero, no subtitle should appear.
+
+10. **Rapid toggle:** Toggle sections open/closed rapidly 10+ times. Check browser console (F12) for "ResizeObserver loop" errors.
+
+11. **Chevron rotation:** When a section is expanded, the chevron should rotate 90 degrees (pointing down). When collapsed, it should point right.
+
+ Type "approved" or describe any issues found
+
+
+
+
+
+- `bun run lint` passes
+- `bun run build` succeeds
+- Dashboard renders collapsible sections for all non-empty category groups
+- Over-budget sections auto-expand, on-budget sections start collapsed
+- Section headers show correct badges, left border accent, and difference
+- Line-item tables have 4 columns with footer totals
+- Carryover subtitle displays on balance card when non-zero
+- Expand/collapse animation is smooth, no ResizeObserver errors
+- Month navigation resets expand/collapse state
+
+
+
+- All 4 ROADMAP success criteria for Phase 3 are met:
+ 1. Category groups render as collapsible sections with color-accented headers, budgeted/actual totals, and difference
+ 2. Expanding reveals line-item table, collapsing hides it with smooth animation, no chart jank
+ 3. Rapid toggling produces no ResizeObserver loop errors
+ 4. Carryover amount visible on balance card when non-zero
+- Human verification checkpoint passes
+
+
+