"use client"; import { useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { BarChart3, Download, LineChart as LineChartIcon } from "lucide-react"; import { WealthChart, type TimelineSeries } from "@/components/WealthChart"; import { SparquoteChart } from "@/components/SparquoteChart"; import { api } from "@/lib/api-client"; import { formatChf } from "@/lib/format"; import type { PlanComputed } from "@/lib/calculations"; import type { PlanInput } from "@/lib/types"; const PALETTE = ["#4f46e5", "#0ea5e9", "#16a34a", "#d97706", "#dc2626", "#7c3äd"]; interface PlanListItem { id: string; name: string; } export function Dashboard({ plan, computed, siblings, }: { plan: PlanInput; computed: PlanComputed; // Die übrigen Szenarien desselben Plans -- nur die sind sinnvoll vergleichbar. siblings: PlanListItem[]; }) { const [compareIds, setCompareIds] = useState([]); const [compareData, setCompareData] = useState>({}); async function toggleCompare(id: string) { if (compareIds.includes(id)) { setCompareIds((prev) => prev.filter((p) => p !== id)); return; } setCompareIds((prev) => [...prev, id]); if (!compareData[id]) { const data = await api.get<{ computed: PlanComputed }>(`/api/scenarios/${id}`); setCompareData((prev) => ({ ...prev, [id]: data.computed })); } } const series: TimelineSeries[] = useMemo(() => { const result: TimelineSeries[] = [{ label: plan.name, color: PALETTE[0], computed }]; compareIds.forEach((id, i) => { const c = compareData[id]; const name = siblings.find((p) => p.id === id)?.name ?? id; if (c) result.push({ label: name, color: PALETTE[(i + 1) % PALETTE.length], computed: c }); }); return result; }, [plan.name, computed, compareIds, compareData, siblings]); const ASSET_CATS = ["PENSION_FUND", "PILLAR_3A", "REAL_ESTATE", "OTHER_ASSET"]; // Asset-Elemente (nach id, damit gleiche Namen nicht kollidieren), die irgendwann einen // positiven Wert haben -- in Reihenfolge ihres ersten Auftretens. const assetEls = useMemo(() => { const info = new Map(); for (const phase of computed.phases) { for (const el of phase.elements) { if (!ASSET_CATS.includes(el.category)) continue; const cur = info.get(el.elementId) ?? { name: el.name, any: false }; cur.name = el.name; if (el.startValue > 0 || el.endValue > 0) cur.any = true; info.set(el.elementId, cur); } } return [...info.entries()].filter(([, v]) => v.any).map(([id, v]) => ({ id, name: v.name })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [computed]); // Je Phase zwei Kategorien auf der x-Achse: Beginn und Ende. Werte je Asset-Element. const barData = useMemo( () => computed.phases.flatMap((phase) => { const beginn: Record = { label: `${phase.name} · Beginn` }; const ende: Record = { label: `${phase.name} · Ende` }; for (const el of phase.elements) { if (!ASSET_CATS.includes(el.category)) continue; beginn[el.elementId] = Math.max(0, Math.round(el.startValue)); ende[el.elementId] = Math.max(0, Math.round(el.endValue)); } return [beginn, ende]; }), // eslint-disable-next-line react-hooks/exhaustive-deps [computed] ); const otherPlans = siblings; const lastPhase = computed.phases[computed.phases.length - 1]; return (

Einkommen vs. Ausgaben pro Jahr

Die Fläche zwischen Einkommen und nominalen Ausgaben ist die Spar- (grün) bzw. Verzehrquote (rot). Die blasse Linie sind die realen Ausgaben – der Abstand zur nominalen Linie ist der Inflationsanteil.

Vermögensverlauf nach Alter

CSV-Export
{otherPlans.length > 0 && (
Szenarien vergleichen: {otherPlans.map((p) => ( ))}
)}

Vermögensaufteilung pro Phase (Beginn & Ende)

Je Phase links die Aufteilung zu Beginn, rechts am Ende. Das Ende einer Phase entspricht im Gesamtvolumen dem Beginn der nächsten – die Aufteilung kann durch Umschichtung abweichen.

Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} /> (typeof v === "number" ? formatChf(v) : v)} /> {assetEls.map((el, i) => ( ))}
); } function StatCard({ label, value, help }: { label: string; value: number; help?: string }) { return (
{label}
{formatChf(value)} CHF
); }