` 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