diff --git a/src/components/dashboard/DashboardSkeleton.tsx b/src/components/dashboard/DashboardSkeleton.tsx
index a41800c..3d0836b 100644
--- a/src/components/dashboard/DashboardSkeleton.tsx
+++ b/src/components/dashboard/DashboardSkeleton.tsx
@@ -52,6 +52,21 @@ export function DashboardSkeleton() {
+
+ {/* Collapsible sections skeleton */}
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
)
}
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
index c6487dc..8b350aa 100644
--- a/src/pages/DashboardPage.tsx
+++ b/src/pages/DashboardPage.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react"
+import { useState, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useBudgets, useBudgetDetail } from "@/hooks/useBudgets"
import { useMonthParam } from "@/hooks/useMonthParam"
@@ -13,6 +13,7 @@ import { MonthNavigator } from "@/components/dashboard/MonthNavigator"
import { ExpenseDonutChart } from "@/components/dashboard/charts/ExpenseDonutChart"
import { IncomeBarChart } from "@/components/dashboard/charts/IncomeBarChart"
import { SpendBarChart } from "@/components/dashboard/charts/SpendBarChart"
+import { CollapsibleSections } from "@/components/dashboard/CollapsibleSections"
import QuickAddPicker from "@/components/QuickAddPicker"
// ---------------------------------------------------------------------------
@@ -27,6 +28,26 @@ const EXPENSE_TYPES: CategoryType[] = [
"investment",
]
+const CATEGORY_TYPES_ALL: CategoryType[] = [
+ "income",
+ "bill",
+ "variable_expense",
+ "debt",
+ "saving",
+ "investment",
+]
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+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
+}
+
// ---------------------------------------------------------------------------
// Dashboard inner — rendered once a budget id is known
// ---------------------------------------------------------------------------
@@ -114,6 +135,33 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
[items, t]
)
+ 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]
+ )
+
+ const [openSections, setOpenSections] = useState>(() =>
+ Object.fromEntries(
+ groupedSections.map((g) => [g.type, isOverBudget(g.type, g.budgeted, g.actual)])
+ )
+ )
+ // Note: state resets automatically on month navigation because DashboardContent
+ // is keyed by budgetId in DashboardPage (key={budgetId} causes full remount)
+
// ------------------------------------------------------------------
// Early returns after all hooks
// ------------------------------------------------------------------
@@ -121,6 +169,10 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
if (loading) return
if (!budget) return null
+ const handleToggleSection = (type: string, open: boolean) => {
+ setOpenSections((prev) => ({ ...prev, [type]: open }))
+ }
+
const currency = budget.currency
const availableBalance = totalIncome - totalExpenses + budget.carryover_amount
@@ -194,6 +246,17 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
+ {/* Collapsible category sections */}
+ {groupedSections.length > 0 && (
+
+ )}
+
{/* Quick Add button */}
@@ -264,7 +327,7 @@ export default function DashboardPage() {
) : (
-
+
)}
)