diff --git a/src/components/dashboard/charts/ChartEmptyState.tsx b/src/components/dashboard/charts/ChartEmptyState.tsx new file mode 100644 index 0000000..5df74a1 --- /dev/null +++ b/src/components/dashboard/charts/ChartEmptyState.tsx @@ -0,0 +1,19 @@ +import { cn } from "@/lib/utils" + +interface ChartEmptyStateProps { + message: string + className?: string +} + +export function ChartEmptyState({ message, className }: ChartEmptyStateProps) { + return ( +
+

{message}

+
+ ) +} diff --git a/src/components/dashboard/charts/ExpenseDonutChart.tsx b/src/components/dashboard/charts/ExpenseDonutChart.tsx new file mode 100644 index 0000000..dc55ba7 --- /dev/null +++ b/src/components/dashboard/charts/ExpenseDonutChart.tsx @@ -0,0 +1,156 @@ +import { useMemo, useState } from "react" +import { PieChart, Pie, Cell, Sector, Label } from "recharts" +import type { PieSectorDataItem } from "recharts/types/polar/Pie" +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart" +import type { ChartConfig } from "@/components/ui/chart" +import { ChartEmptyState } from "./ChartEmptyState" +import { formatCurrency } from "@/lib/format" + +interface ExpenseDonutChartProps { + data: Array<{ type: string; value: number; label: string }> + totalExpenses: number + currency: string + emptyMessage: string +} + +function renderActiveShape(props: PieSectorDataItem) { + const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = + props + return ( + + ) +} + +export function ExpenseDonutChart({ + data, + totalExpenses, + currency, + emptyMessage, +}: ExpenseDonutChartProps) { + const [activeIndex, setActiveIndex] = useState(-1) + + const chartConfig = useMemo(() => { + const config: ChartConfig = {} + for (const entry of data) { + config[entry.type] = { + label: entry.label, + color: `var(--color-${entry.type}-fill)`, + } + } + return config + }, [data]) + + // No data at all: show empty state placeholder + if (data.length === 0 && totalExpenses === 0) { + return + } + + // Zero-amount state: budget exists but all actuals are zero + const isAllZero = totalExpenses === 0 + const displayData = isAllZero + ? [{ type: "empty", value: 1, label: "" }] + : data + const displayConfig: ChartConfig = isAllZero + ? { empty: { label: "", color: "var(--color-muted)" } } + : chartConfig + + return ( +
+ + + + formatCurrency(Number(value), currency) + } + /> + } + /> + setActiveIndex(index) + } + onMouseLeave={ + isAllZero ? undefined : () => setActiveIndex(-1) + } + > + {displayData.map((entry) => ( + + ))} + + + + + {/* Custom legend below the donut */} + {!isAllZero && data.length > 0 && ( +
    + {data.map((entry) => ( +
  • + + {entry.label} + + {formatCurrency(entry.value, currency)} + +
  • + ))} +
+ )} +
+ ) +}