V4-Rechenmodell: Flow-Indexierung, Cash-Ausgleichstopf, Ruin-Erkennung
Deploy App / deploy (push) Successful in 3m6s

Behebt die Nominal/Real-Inkonsistenz (Bug #1): Einkommen/Ausgaben werden neu Jahr fuer
Jahr indexiert (eigenes Feld teuerungsausgleich je Element, Default = Phaseninflation;
Renten nominal fix = 0%). Vermoegen verzinst weiterhin nominal.

Cash: neues systemseitiges, immer sichtbares Element (0% Verzinsung, kein DB-Row -
synthetisch in computePlan). Ist der Ausgleichstopf = "verfuegbares Kapital fuer
Investments": cash_delta(t) = quote(t) - geplante flache Jahresraten (3a/Vermoegen/Amort./
Tilgung); Cash laeuft ueber Phasen fort, darf negativ werden (rot). Der Verteilzwang und
die harten Sparraten-Caps entfallen.

Ruin: Gesamtvermoegen (inkl. Cash) je Jahr; erstes Unterschreiten von 0 -> Ruin-Alter
(Person A), Anzeige als Banner + Zeitachsen-Marker.

Phasenkopf neu: Einkommen/Ausgaben Start->Ende, Quote Beginn/Ende, Cash Start->Ende,
Vermoegen inkl. Cash, Realwert. Inflation-Deflator neu pro Jahr (Math.pow ^Dauer).

Golden Tests (vitest) 1-4 + Renten-0% + Fortschreibung gruen (Test1 761'654/565'928,
Test2 Ruin Alter 94).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:24:13 +02:00
parent 9506f43b67
commit 5e17ce4afb
10 changed files with 1349 additions and 299 deletions
+23 -1
View File
@@ -13,7 +13,15 @@ interface PersonAxis {
// Horizontale Zeitachse: Alter von links nach rechts, mit deutlich markiertem
// Pensionsalter je Person und Trennlinien an den Phasengrenzen.
export function Timeline({ phases, persons }: { phases: PhaseComputed[]; persons: PersonAxis[] }) {
export function Timeline({
phases,
persons,
ruinAge,
}: {
phases: PhaseComputed[];
persons: PersonAxis[];
ruinAge?: number | null;
}) {
if (phases.length === 0 || persons.length === 0) return null;
const totalYears = phases.reduce((s, p) => s + p.durationYears, 0);
@@ -64,6 +72,20 @@ export function Timeline({ phases, persons }: { phases: PhaseComputed[]; persons
) : null
)}
{/* Ruin-Marker (Person A) */}
{ruinAge != null && ruinAge > minAge && ruinAge <= maxAge && (
<div
className="absolute top-0 flex -translate-x-1/2 flex-col items-center"
style={{ left: pct(ruinAge) }}
title={`Kapital aufgebraucht mit Alter ${ruinAge}`}
>
<span className="text-[10px] font-semibold" style={{ color: "var(--danger)" }}>
Ruin {ruinAge}
</span>
<div className="mt-0.5 h-4 w-px" style={{ backgroundColor: "var(--danger)" }} />
</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) => (