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() {
) : ( - + )} )