UI-Umbau: Analyse-Bereich, Planstart, Zeitachse mit Phasen
Deploy App / deploy (push) Successful in 1m44s
Deploy App / deploy (push) Successful in 1m44s
Sieben Anpassungen aus dem Feedback: - Grafiken liegen neu im eigenen Bereich "Grafiken" (Dialog, Button oben) statt unter der Matrix. Die Matrix bleibt die ruhige Hauptansicht. - Kennzahl "Geschaetzter Nachlass" entfernt -- sie war rechnerisch identisch mit dem nominalen Endvermoegen und suggerierte eine Zusatzinformation, die es nicht gab. - Monte-Carlo-Button nach oben zu den Szenario-Aktionen verschoben. - Neues Profilfeld "Planstart (Jahr)" (Scenario.startYear + Migration). Rein fuer die Darstellung -- die Berechnung rechnet unveraendert in relativen Jahren. Bestehende Szenarien werden auf das laufende Jahr gesetzt. - Zeitachse zeigt die Lebensphasen als Segmente (Breite = Dauer, Einfaerbung nach Phasentyp) inkl. Jahresspanne, plus Jahres-Beschriftung an den Enden. - Lebensphase bearbeiten neu als Popup statt Panel unter der Tabelle -- konsistent zu allen anderen Eingaben; Speichern schliesst. - Vermoegensverlauf ueber ALLE Jahre statt nur ueber die Phasengrenzen. Dafuer fuehrt computePlan das Vermoegen neu pro Jahr mit (YearPoint.wealthNominal/ wealthReal) -- dieselbe Groesse, die schon die Ruin-Erkennung berechnet. Damit werden Verlaeufe INNERHALB einer Phase sichtbar (z. B. Kapitalverzehr). Zwei Tests fuer das Jahres-Vermoegen (58 -> 60). Spezifikation auf v0.9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
-23
@@ -11,16 +11,19 @@ interface PersonAxis {
|
||||
color: string;
|
||||
}
|
||||
|
||||
// Horizontale Zeitachse: Alter von links nach rechts, mit deutlich markiertem
|
||||
// Pensionsalter je Person und Trennlinien an den Phasengrenzen.
|
||||
// Horizontale Zeitachse: die Lebensphasen als aneinandergereihte Segmente (Breite = Dauer),
|
||||
// darunter Alter und -- sofern ein Planstart gesetzt ist -- die Kalenderjahre. Pensionsalter
|
||||
// je Person und ein allfaelliges Ruinalter sind als Marker eingezeichnet.
|
||||
export function Timeline({
|
||||
phases,
|
||||
persons,
|
||||
ruinAge,
|
||||
startYear,
|
||||
}: {
|
||||
phases: PhaseComputed[];
|
||||
persons: PersonAxis[];
|
||||
ruinAge?: number | null;
|
||||
startYear?: number | null;
|
||||
}) {
|
||||
if (phases.length === 0 || persons.length === 0) return null;
|
||||
|
||||
@@ -30,18 +33,18 @@ export function Timeline({
|
||||
const span = Math.max(1, maxAge - minAge);
|
||||
|
||||
const pct = (age: number) => `${(Math.max(0, Math.min(span, age - minAge)) / span) * 100}%`;
|
||||
const yearOf = (yearsFromStart: number) => (startYear ? startYear + yearsFromStart : null);
|
||||
|
||||
// Phasengrenzen (kumulierte Jahre).
|
||||
const boundaries: { year: number; label: string }[] = [];
|
||||
let acc = 0;
|
||||
for (const p of phases) {
|
||||
boundaries.push({ year: acc, label: p.name });
|
||||
acc += p.durationYears;
|
||||
}
|
||||
// Segmente: Start-/Endjahr (relativ zum Planbeginn) je Phase -- ohne Mutation waehrend
|
||||
// des Renderns, deshalb die kumulierte Summe der vorangehenden Dauern.
|
||||
const segments = phases.map((p, i) => {
|
||||
const from = phases.slice(0, i).reduce((s, x) => s + x.durationYears, 0);
|
||||
return { phase: p, from, to: from + p.durationYears };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold text-fg">Zeitachse</h3>
|
||||
<div className="flex gap-3 text-xs text-muted">
|
||||
{persons.map((p) => (
|
||||
@@ -61,7 +64,9 @@ export function Timeline({
|
||||
key={p.role}
|
||||
className="absolute top-0 flex -translate-x-1/2 flex-col items-center"
|
||||
style={{ left: pct(p.retirementAge) }}
|
||||
title={`${p.label}: Pensionierung mit ${p.retirementAge}`}
|
||||
title={`${p.label}: Pensionierung mit ${p.retirementAge}${
|
||||
yearOf(p.retirementAge - minAge) ? ` (${yearOf(p.retirementAge - minAge)})` : ""
|
||||
}`}
|
||||
>
|
||||
<Flag className="h-3.5 w-3.5" style={{ color: p.color }} fill={p.color} />
|
||||
<span className="whitespace-nowrap text-[10px] font-medium" style={{ color: p.color }}>
|
||||
@@ -86,21 +91,39 @@ export function Timeline({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Achse */}
|
||||
<div className="relative h-2 w-full rounded-full bg-gradient-to-r from-accent-soft to-accent">
|
||||
{boundaries.slice(1).map((b) => (
|
||||
<div
|
||||
key={b.year}
|
||||
className="absolute top-0 h-2 w-px bg-surface/70"
|
||||
style={{ left: pct(minAge + b.year) }}
|
||||
/>
|
||||
))}
|
||||
{/* Phasen-Segmente: Breite proportional zur Dauer, Einfaerbung nach Phasentyp. */}
|
||||
<div className="flex w-full overflow-hidden rounded-lg border border-border">
|
||||
{segments.map((s, i) => {
|
||||
const soft = s.phase.type === "PENSION" || s.phase.type === "MIXED";
|
||||
return (
|
||||
<div
|
||||
key={s.phase.id}
|
||||
style={{ width: `${(s.phase.durationYears / span) * 100}%` }}
|
||||
title={`${s.phase.name} · ${s.phase.durationYears} Jahre`}
|
||||
className={`flex min-w-0 flex-col justify-center px-2 py-1.5 ${
|
||||
i > 0 ? "border-l border-border" : ""
|
||||
} ${s.phase.type === "PENSION" ? "bg-accent-soft" : s.phase.type === "MIXED" ? "bg-accent/25" : "bg-accent/70"}`}
|
||||
>
|
||||
<span className={`truncate text-[10px] font-semibold ${soft ? "text-accent-soft-fg" : "text-accent-fg"}`}>
|
||||
{s.phase.name}
|
||||
</span>
|
||||
<span className={`truncate text-[9px] ${soft ? "text-accent-soft-fg" : "text-accent-fg"} opacity-80`}>
|
||||
{s.phase.durationYears} J.
|
||||
{yearOf(s.from) ? ` · ${yearOf(s.from)}–${yearOf(s.to)}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Alters-Beschriftung */}
|
||||
{/* Alters- und Jahres-Beschriftung */}
|
||||
<div className="mt-1 flex justify-between text-[11px] text-muted">
|
||||
<span>{minAge} J.</span>
|
||||
<span>{maxAge} J.</span>
|
||||
<span>
|
||||
{minAge} J.{yearOf(0) ? ` · ${yearOf(0)}` : ""}
|
||||
</span>
|
||||
<span>
|
||||
{maxAge} J.{yearOf(totalYears) ? ` · ${yearOf(totalYears)}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user