"use client"; import { useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { BookOpen, X } from "lucide-react"; import { formatChf } from "@/lib/format"; import { CATEGORY_LABELS } from "@/lib/elements"; import type { ElementCategory } from "@/lib/elements"; import type { CashBridge, ElementYearPoint, PhaseComputed, PlanComputed, Trace, TraceStep, WealthBridge, } from "@/lib/calculations"; // --- Wasserfall --------------------------------------------------------------------------- // Bewusst NICHT mit Recharts, sondern als eigene HTML/CSS-Darstellung. Ein Wasserfall lebt // von drei Dingen, die Recharts hier nicht hergibt: Verbindungslinien zwischen den Balken // (ohne sie sieht man nicht, dass jeder Balken dort ansetzt, wo der vorherige aufhört), // Wertbeschriftung an jedem Balken, und eine klare optische Trennung von Zwischenständen // und Veränderungen. // // Liegend statt stehend: Die Beschriftungen sind lang ("Wertsteigerung Immobilie"), stehend // müssten sie gedreht werden. Liegend ist es ausserdem konsistent zum Tornado. interface WaterfallItem { label: string; value: number; total?: boolean; // Zwischen-/Endsumme: absoluter Stand statt Veränderung section?: string; // optionale Abschnitts-Überschrift VOR diesem Eintrag } interface WaterfallRow { label: string; section?: string; from: number; to: number; value: number; running: number; // Stand NACH diesem Schritt kind: "total" | "pos" | "neg"; } function waterfallRows(items: WaterfallItem[]): WaterfallRow[] { let running = 0; return items.map((it) => { if (it.total) { running = it.value; return { label: it.label, section: it.section, from: 0, to: it.value, value: it.value, running, kind: "total" as const }; } const from = running; running += it.value; return { label: it.label, section: it.section, from, to: running, value: it.value, running, kind: (it.value >= 0 ? "pos" : "neg") as "pos" | "neg", }; }); } const WF_FILL = { total: "var(--accent)", pos: "#16a34a", neg: "#dc2626" }; const ROW_H = 34; const BAR_H = 20; function Waterfall({ items }: { items: WaterfallItem[] }) { const rows = useMemo(() => waterfallRows(items), [items]); if (rows.length === 0) return null; const lo = Math.min(0, ...rows.map((r) => Math.min(r.from, r.to))); const hi = Math.max(0, ...rows.map((r) => Math.max(r.from, r.to))); const span = hi - lo || 1; const pos = (v: number) => ((v - lo) / span) * 100; return (
| Schritt | Betrag | Zwischenstand |
|---|---|---|
| {r.label} | {r.kind === "total" ? "—" : `${r.value >= 0 ? "+" : "−"}${formatChf(Math.abs(r.value))}`} | {formatChf(r.running)} |
Die Zerlegung geht nicht auf. Nicht zugeordnete Differenz: {formatChf(residual)}. Bitte melden – das ist ein Fehler in der Berechnung, nicht in der Darstellung.
); } function bridgeItems(w: WealthBridge, isFirst: boolean): WaterfallItem[] { const items: WaterfallItem[] = []; if (!isFirst) { items.push({ label: "Vermögen Ende Vorphase", value: w.openingWealth, total: true, section: "Am Übergang in diese Phase" }); if (w.oneOffInflow) items.push({ label: "Einmaliger Zufluss", value: w.oneOffInflow }); if (w.oneOffOutflow) items.push({ label: "Einmalige Kosten", value: -w.oneOffOutflow }); if (w.transitionTax) items.push({ label: "Steuern am Übergang", value: -w.transitionTax }); if (w.pensionConversion) items.push({ label: "PK in Rente umgewandelt", value: -w.pensionConversion }); if (w.saleGainLoss) items.push({ label: "Verkaufsdifferenz", value: w.saleGainLoss }); } items.push({ label: "Vermögen Phasenbeginn", value: w.startWealth, total: true, section: isFirst ? undefined : "Innerhalb der Phase", }); if (w.quotaTotal) items.push({ label: "Spar-/Verzehrquote", value: w.quotaTotal }); if (w.investmentReturn) items.push({ label: "Kapitalerträge", value: w.investmentReturn }); if (w.propertyAppreciation) items.push({ label: "Wertsteigerung Immobilie", value: w.propertyAppreciation }); if (w.pensionFundContribution) items.push({ label: "PK-Beiträge", value: w.pensionFundContribution }); items.push({ label: "Vermögen Phasenende", value: w.endWealth, total: true }); return items; } function cashItems(c: CashBridge, isFirst: boolean): WaterfallItem[] { const items: WaterfallItem[] = []; items.push({ label: isFirst ? "Cash-Anfangswert" : "Cash Ende Vorphase", value: c.openingCash, total: true, section: isFirst ? undefined : "Am Übergang in diese Phase", }); if (c.capitalInflow) items.push({ label: "Kapitalzufluss", value: c.capitalInflow }); if (c.oneOffInflow) items.push({ label: "Einmaliger Zufluss", value: c.oneOffInflow }); if (c.immediateRepay) items.push({ label: "Sofort-Tilgung", value: -c.immediateRepay }); if (c.oneOffOutflow) items.push({ label: "Einmalige Kosten", value: -c.oneOffOutflow }); if (c.investments) items.push({ label: "Investitionen", value: -c.investments }); items.push({ label: "Cash Phasenbeginn", value: c.cashStart, total: true, section: "Innerhalb der Phase" }); if (c.quotaTotal) items.push({ label: "Spar-/Verzehrquote", value: c.quotaTotal }); if (c.savingRates) items.push({ label: "Sparraten", value: -c.savingRates }); if (c.debtRates) items.push({ label: "Amort./Tilgung", value: -c.debtRates }); if (c.withdrawals) items.push({ label: "Bezugsraten", value: c.withdrawals }); items.push({ label: "Cash Phasenende", value: c.cashEnd, total: true }); return items; } // --- Rechenweg ---------------------------------------------------------------------------- function TraceStepRow({ step }: { step: TraceStep }) { const unit = step.unit ?? "CHF"; const value = unit === "" ? step.substituted ?? "" : unit === "%" ? `${step.result} %` : unit === "Jahre" ? `${step.result}` : formatChf(step.result); return ({subtitle}
}Nur-Lese-Ansicht. Werte werden hier nicht verändert.
Für dieses Element gibt es in diesem Plan keinen Verlauf.
) : (| Lebensphase | Beginn | Ende | Hinweis |
|---|---|---|---|
| {phase.name} | {formatChf(ec!.startValue)} | {formatChf(ec!.endValue)} | {ec!.note ?? ""} |
Für dieses Element gibt es keinen eigenen Rechenweg.
)} {perPhase.map(({ phase, ec }) => (Zusammensetzung des Anlagevermögens zu Beginn und am Ende dieser Phase.
{allocEls.length === 0 ? (In dieser Phase gibt es kein Anlagevermögen.
) : (Nur echte Zu- und Abgänge. Sparraten, Amortisationen und Zusatzinvestitionen erscheinen hier bewusst nicht: Sie verschieben Geld vom Cash in einen Vermögenswert, ohne das Vermögen zu verändern – als Balken gezeichnet würden sie einen Verlust vortäuschen, den es nicht gibt.
Hier erscheinen die Umbuchungen: Sparraten, Amortisationen und Investitionen verlassen das Cash-Konto, auch wenn sie das Vermögen nicht mindern.
Kein Rechenweg verfügbar.
} {(phase.traces ?? []).map((t, i) => (