Rework core model: financial elements as plan-wide entities across phases; derived phase types (Erwerb/Pension/Misch) with retirement-capped durations and per-plan retirement age; AHV (gap years + couple ceiling), PK payout/annuity, 3a, real estate, other assets/debts; horizontal timeline with retirement markers; phase x element matrix with detail panel; savings/consumption quota + available-capital key figures with red status
Deploy App / deploy (push) Successful in 1m57s
Deploy App / deploy (push) Successful in 1m57s
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
@@ -20,59 +19,41 @@ export interface TimelineSeries {
|
||||
computed: PlanComputed;
|
||||
}
|
||||
|
||||
function buildTimeline(computed: PlanComputed) {
|
||||
const points: { year: number; nominal: number; real: number }[] = [
|
||||
{ year: 0, nominal: computed.phases[0]?.startWealthNominal ?? 0, real: computed.phases[0]?.startWealthNominal ?? 0 },
|
||||
];
|
||||
const boundaries: { year: number; name: string }[] = [];
|
||||
let year = 0;
|
||||
for (const phase of computed.phases) {
|
||||
boundaries.push({ year, name: phase.name });
|
||||
for (let y = 0; y < phase.durationYears; y++) {
|
||||
year += 1;
|
||||
points.push({ year, nominal: phase.yearlyNominal[y], real: phase.yearlyReal[y] });
|
||||
}
|
||||
}
|
||||
return { points, boundaries };
|
||||
}
|
||||
|
||||
// Liniendiagramm ueber alle Phasen, nominal + real, mit Markierungen an den
|
||||
// Phasengrenzen (TDD Kapitel 4.5 / 14). Unterstuetzt optional mehrere ueberlagerte
|
||||
// Plaene fuer den Szenario-Vergleich.
|
||||
// Liniendiagramm: Endvermoegen (nominal + real) je Lebensphase. Unterstuetzt mehrere
|
||||
// ueberlagerte Plaene fuer den Szenario-Vergleich.
|
||||
export function WealthChart({ series }: { series: TimelineSeries[] }) {
|
||||
if (series.length === 0 || series[0].computed.phases.length === 0) {
|
||||
return <p className="text-sm text-zinc-500">Noch keine Phasen vorhanden.</p>;
|
||||
}
|
||||
|
||||
const primary = buildTimeline(series[0].computed);
|
||||
const maxYear = Math.max(...series.map((s) => buildTimeline(s.computed).points.length - 1));
|
||||
|
||||
const merged: Record<number, Record<string, number>> = {};
|
||||
for (const s of series) {
|
||||
const tl = buildTimeline(s.computed);
|
||||
for (const p of tl.points) {
|
||||
merged[p.year] = merged[p.year] ?? { year: p.year };
|
||||
merged[p.year][`${s.label} (nominal)`] = p.nominal;
|
||||
merged[p.year][`${s.label} (real)`] = p.real;
|
||||
// Datenpunkte je Phasen-Index; X-Achse = Phasenname des Hauptplans.
|
||||
const maxLen = Math.max(...series.map((s) => s.computed.phases.length));
|
||||
const data = Array.from({ length: maxLen }, (_, i) => {
|
||||
const row: Record<string, number | string> = {
|
||||
phase: series[0].computed.phases[i]?.name ?? `Phase ${i + 1}`,
|
||||
};
|
||||
for (const s of series) {
|
||||
const p = s.computed.phases[i];
|
||||
if (p) {
|
||||
row[`${s.label} (nominal)`] = Math.round(p.endWealthNominal);
|
||||
row[`${s.label} (real)`] = Math.round(p.endWealthReal);
|
||||
}
|
||||
}
|
||||
}
|
||||
const data = Array.from({ length: maxYear + 1 }, (_, y) => merged[y] ?? { year: y });
|
||||
return row;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-80 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-zinc-200 dark:stroke-zinc-700" />
|
||||
<XAxis dataKey="year" tick={{ fontSize: 11 }} label={{ value: "Jahr", position: "insideBottomRight", offset: -4, fontSize: 11 }} />
|
||||
<XAxis dataKey="phase" tick={{ fontSize: 11 }} />
|
||||
<YAxis
|
||||
tick={{ fontSize: 11 }}
|
||||
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
|
||||
/>
|
||||
<Tooltip formatter={(v) => (typeof v === "number" ? formatChf(v) : v)} />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
{primary.boundaries.slice(1).map((b) => (
|
||||
<ReferenceLine key={b.year} x={b.year} stroke="#a1a1aa" strokeDasharray="2 2" />
|
||||
))}
|
||||
{series.map((s) => (
|
||||
<Line
|
||||
key={`${s.label}-nominal`}
|
||||
|
||||
Reference in New Issue
Block a user