feat(03-02): wire collapsible sections into DashboardContent
- Add CATEGORY_TYPES_ALL constant and isOverBudget direction-aware helper
- Derive groupedSections via useMemo (filters empty groups, computes totals)
- Initialize openSections state with smart defaults (over-budget expanded)
- State resets on month navigation via key={budgetId} on DashboardContent
- Insert CollapsibleSections between chart grid and QuickAdd
- Add skeleton placeholders for collapsible sections area in DashboardSkeleton
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof g> => g !== null),
|
||||
[items, t]
|
||||
)
|
||||
|
||||
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() =>
|
||||
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 <DashboardSkeleton />
|
||||
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 }) {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Collapsible category sections */}
|
||||
{groupedSections.length > 0 && (
|
||||
<CollapsibleSections
|
||||
groups={groupedSections}
|
||||
currency={currency}
|
||||
openSections={openSections}
|
||||
onToggleSection={handleToggleSection}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick Add button */}
|
||||
<div className="flex justify-end">
|
||||
<QuickAddPicker budgetId={budgetId} />
|
||||
@@ -264,7 +327,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DashboardContent budgetId={currentBudget.id} />
|
||||
<DashboardContent key={currentBudget.id} budgetId={currentBudget.id} />
|
||||
)}
|
||||
</PageShell>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user