Dashboard-Grafiken neu: Asset-Aufteilung Beginn/Ende je Phase; Vermoegensverlauf nach Alter
Deploy App / deploy (push) Successful in 56s

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 08:59:28 +02:00
parent f8d77ef864
commit bbe5acee85
2 changed files with 70 additions and 31 deletions
+29 -14
View File
@@ -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<string>();
// 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<string, { name: string; any: boolean }>();
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<string, number | string> = { phase: phase.name };
computed.phases.flatMap((phase) => {
const beginn: Record<string, number | string> = { label: `${phase.name} · Beginn` };
const ende: Record<string, number | string> = { 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({
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-fg">
<LineChartIcon className="h-4 w-4 text-accent" />
Vermoegensverlauf
Vermoegensverlauf nach Alter
</h3>
<a
href={`/api/plans/${plan.id}/export`}
@@ -119,23 +130,27 @@ export function Dashboard({
</section>
<section className="rounded-xl border border-border bg-surface p-4 shadow-sm">
<h3 className="mb-3 flex items-center gap-1.5 text-sm font-semibold text-fg">
<h3 className="mb-1 flex items-center gap-1.5 text-sm font-semibold text-fg">
<BarChart3 className="h-4 w-4 text-accent" />
Vermoegensaufteilung pro Phase (Endvermoegen)
Vermoegensaufteilung pro Phase (Beginn &amp; Ende)
</h3>
<p className="mb-3 text-xs text-muted">
Je Phase links die Aufteilung zu Beginn, rechts am Ende. Das Ende einer Phase entspricht im
Gesamtvolumen dem Beginn der naechsten &ndash; die Aufteilung kann durch Umschichtung abweichen.
</p>
<div className="h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={barData} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis dataKey="phase" tick={{ fontSize: 11 }} />
<XAxis dataKey="label" tick={{ fontSize: 10 }} interval={0} angle={-30} textAnchor="end" height={70} />
<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 }} />
{barKeys.map((key, i) => (
<Bar key={key} dataKey={key} stackId="a" fill={PALETTE[i % PALETTE.length]} />
{assetEls.map((el, i) => (
<Bar key={el.id} dataKey={el.id} name={el.name} stackId="a" fill={PALETTE[i % PALETTE.length]} />
))}
</BarChart>
</ResponsiveContainer>