From bbe5acee851b93aee0e726ea60ce2ea86da02608 Mon Sep 17 00:00:00 2001 From: kelle Date: Tue, 14 Jul 2026 08:59:28 +0200 Subject: [PATCH] Dashboard-Grafiken neu: Asset-Aufteilung Beginn/Ende je Phase; Vermoegensverlauf nach Alter - Balkendiagramm: je Phase zwei x-Kategorien (Beginn/Ende), gestapelt nach Asset-Element (Schluessel = elementId, damit gleiche Namen nicht kollidieren). Zeigt die Umschichtung; Ende Phase N liegt neben Beginn Phase N+1. - Liniendiagramm: x-Achse ist neu das Alter (Referenz Person A) von Startalter bis Ende der letzten Phase, mit Start- und Phasen-Endwerten (nominal + real), statt Phasenindex. Co-Authored-By: Claude Opus 4.8 --- src/components/Dashboard.tsx | 43 +++++++++++++++++-------- src/components/WealthChart.tsx | 58 ++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 7e51df8..a362623 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -51,25 +51,36 @@ export function Dashboard({ }, [plan.name, computed, compareIds, compareData, allPlans]); const ASSET_CATS = ["PENSION_FUND", "PILLAR_3A", "REAL_ESTATE", "OTHER_ASSET"]; - const barKeys = useMemo(() => { - const keys = new Set(); + + // 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) && el.endValue > 0) keys.add(el.name); + 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 Array.from(keys); + 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.map((phase) => { - const row: Record = { phase: phase.name }; + 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) && el.endValue > 0) row[el.name] = el.endValue; + 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 row; + return [beginn, ende]; }), // eslint-disable-next-line react-hooks/exhaustive-deps [computed] @@ -90,7 +101,7 @@ export function Dashboard({

- Vermoegensverlauf + Vermoegensverlauf nach Alter

-

+

- Vermoegensaufteilung pro Phase (Endvermoegen) + Vermoegensaufteilung pro Phase (Beginn & Ende)

+

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

- + Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} /> (typeof v === "number" ? formatChf(v) : v)} /> - {barKeys.map((key, i) => ( - + {assetEls.map((el, i) => ( + ))} diff --git a/src/components/WealthChart.tsx b/src/components/WealthChart.tsx index c4828a7..eb64380 100644 --- a/src/components/WealthChart.tsx +++ b/src/components/WealthChart.tsx @@ -19,25 +19,38 @@ export interface TimelineSeries { computed: PlanComputed; } -// Liniendiagramm: Endvermoegen (nominal + real) je Lebensphase. Unterstuetzt mehrere +// Datenpunkte je Serie: Start (Beginn Phase 1) + Endwert je Phase, verortet auf dem +// Alter der Referenzperson (Person A). So laeuft die x-Achse ueber das Alter statt ueber +// die Phasen. +function pointsFor(computed: PlanComputed) { + const phases = computed.phases; + if (phases.length === 0) return [] as { age: number; nominal: number; real: number }[]; + const refAge = (ph: PlanComputed["phases"][number]) => ph.persons[0]?.startAge ?? 0; + const pts = [ + { age: refAge(phases[0]), nominal: Math.round(phases[0].startWealthNominal), real: Math.round(phases[0].startWealthNominal) }, + ]; + for (const p of phases) { + pts.push({ age: refAge(p) + p.durationYears, nominal: Math.round(p.endWealthNominal), real: Math.round(p.endWealthReal) }); + } + return pts; +} + +// Liniendiagramm: Gesamtvermoegen (nominal + real) ueber das Alter. 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

Noch keine Phasen vorhanden.

; } - // 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 = { - 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 withPoints = series.map((s) => ({ ...s, points: pointsFor(s.computed) })); + const ages = Array.from(new Set(withPoints.flatMap((s) => s.points.map((p) => p.age)))).sort((a, b) => a - b); + + const data = ages.map((age) => { + const row: Record = { age }; + for (const s of withPoints) { + const pt = s.points.find((p) => p.age === age); + row[`${s.label} (nominal)`] = pt ? pt.nominal : null; + row[`${s.label} (real)`] = pt ? pt.real : null; } return row; }); @@ -47,14 +60,23 @@ export function WealthChart({ series }: { series: TimelineSeries[] }) { - + `${v} J.`} + /> Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} /> - (typeof v === "number" ? formatChf(v) : v)} /> + (typeof v === "number" ? formatChf(v) : v)} + labelFormatter={(v) => `Alter ${v}`} + /> - {series.map((s) => ( + {withPoints.map((s) => ( ))} - {series.map((s) => ( + {withPoints.map((s) => ( ))}