diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
index 6f8c97b..04c10ff 100644
--- a/src/pages/DashboardPage.tsx
+++ b/src/pages/DashboardPage.tsx
@@ -1,20 +1,18 @@
-import { Link } from "react-router-dom"
+import { useMemo } from "react"
import { useTranslation } from "react-i18next"
-import {
- PieChart,
- Pie,
- Cell,
- ResponsiveContainer,
- Tooltip,
-} from "recharts"
import { useBudgets, useBudgetDetail } from "@/hooks/useBudgets"
+import { useMonthParam } from "@/hooks/useMonthParam"
import type { CategoryType } from "@/lib/types"
-import { categoryColors } from "@/lib/palette"
import { formatCurrency } from "@/lib/format"
+import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { PageShell } from "@/components/shared/PageShell"
import { SummaryStrip } from "@/components/dashboard/SummaryStrip"
import { DashboardSkeleton } from "@/components/dashboard/DashboardSkeleton"
+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 QuickAddPicker from "@/components/QuickAddPicker"
// ---------------------------------------------------------------------------
@@ -29,18 +27,6 @@ const EXPENSE_TYPES: CategoryType[] = [
"investment",
]
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-/**
- * Returns the ISO date string for the first day of the given month.
- * e.g. currentMonthStart(2026, 3) => "2026-03-01"
- */
-function currentMonthStart(year: number, month: number): string {
- return `${year}-${String(month).padStart(2, "0")}-01`
-}
-
// ---------------------------------------------------------------------------
// Dashboard inner — rendered once a budget id is known
// ---------------------------------------------------------------------------
@@ -49,67 +35,97 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
const { t } = useTranslation()
const { budget, items, loading } = useBudgetDetail(budgetId)
+ // ------------------------------------------------------------------
+ // Derived totals — all hooks must be called before any early returns
+ // ------------------------------------------------------------------
+
+ const totalIncome = useMemo(
+ () =>
+ items
+ .filter((i) => i.category?.type === "income")
+ .reduce((sum, i) => sum + i.actual_amount, 0),
+ [items]
+ )
+
+ const totalExpenses = useMemo(
+ () =>
+ items
+ .filter((i) => i.category?.type !== "income")
+ .reduce((sum, i) => sum + i.actual_amount, 0),
+ [items]
+ )
+
+ const budgetedIncome = useMemo(
+ () =>
+ items
+ .filter((i) => i.category?.type === "income")
+ .reduce((sum, i) => sum + i.budgeted_amount, 0),
+ [items]
+ )
+
+ const budgetedExpenses = useMemo(
+ () =>
+ items
+ .filter((i) => i.category?.type !== "income")
+ .reduce((sum, i) => sum + i.budgeted_amount, 0),
+ [items]
+ )
+
+ // ------------------------------------------------------------------
+ // Chart data derivations (memoized)
+ // ------------------------------------------------------------------
+
+ const pieData = useMemo(
+ () =>
+ EXPENSE_TYPES.map((type) => {
+ const total = items
+ .filter((i) => i.category?.type === type)
+ .reduce((sum, i) => sum + i.actual_amount, 0)
+ return { type, value: total, label: t(`categories.types.${type}`) }
+ }).filter((d) => d.value > 0),
+ [items, t]
+ )
+
+ const incomeBarData = useMemo(() => {
+ const budgeted = items
+ .filter((i) => i.category?.type === "income")
+ .reduce((sum, i) => sum + i.budgeted_amount, 0)
+ const actual = items
+ .filter((i) => i.category?.type === "income")
+ .reduce((sum, i) => sum + i.actual_amount, 0)
+ if (budgeted === 0 && actual === 0) return []
+ return [{ label: t("categories.types.income"), budgeted, actual }]
+ }, [items, t])
+
+ const spendBarData = useMemo(
+ () =>
+ EXPENSE_TYPES.map((type) => {
+ const groupItems = items.filter((i) => i.category?.type === type)
+ if (groupItems.length === 0) return null
+ const budgeted = groupItems.reduce((sum, i) => sum + i.budgeted_amount, 0)
+ const actual = groupItems.reduce((sum, i) => sum + i.actual_amount, 0)
+ return { type, label: t(`categories.types.${type}`), budgeted, actual }
+ }).filter(Boolean) as Array<{
+ type: string
+ label: string
+ budgeted: number
+ actual: number
+ }>,
+ [items, t]
+ )
+
+ // ------------------------------------------------------------------
+ // Early returns after all hooks
+ // ------------------------------------------------------------------
+
if (loading) return
{t("dashboard.noBudget")}
- - {t("budgets.newBudget")} - +{t("dashboard.noBudgetForMonth")}
+