V5-Modell: Einkommen nominal, Ausgaben real->nominal, Inflation plan-weit, Sparquoten-Grafik
Deploy App / deploy (push) Successful in 57s

- Einkommen: nominale Basis + nominale Lohnerhoehung (Default 0). Real read-only als Info.
- Ausgaben: REALE Basis (heutige Kaufkraft) + reale Mehrausgaben (Default 0); nominal =
  real x plan-weite Inflation, read-only. Loest die "nominale Ausgaben verwirren"-Problematik.
- Inflation nur noch plan-weit (Phasen-Override entfernt).
- Engine liefert flowDeflatorEnd (Flow-Realwerte) + yearly-Serie (Jahr/Alter/Einkommen/
  Ausgabe nominal/real). Nominal/Real-Umschalter nutzt Flow- vs. Bestands-Deflatoren.
- Neue Grafik (Dashboard): Einkommen vs. nominale Ausgabe mit eingefaerbter Sparquoten-
  Flaeche (gruen/rot) + reale Ausgabe als Referenzlinie (Inflationskeil).
- In-Tool-Erklaerungen in den Einkommen/Ausgaben-Popups.
- Golden Tests aufs neue Modell hergeleitet (8 gruen, u.a. Ausgaben real->nominal, Ruin 94).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 22:20:06 +02:00
parent 310ebc66fb
commit 6402583e0c
7 changed files with 212 additions and 80 deletions
+30 -15
View File
@@ -24,7 +24,8 @@ export interface CellContext {
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
phaseInflation: number; // Default fuer Teuerungsausgleich
phaseInflation: number; // Plan-Inflationsrate (Info)
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
}
interface Props {
@@ -136,28 +137,42 @@ export function ElementPhaseFields({
case "INCOME":
case "EXPENSE": {
const isIncome = element.category === "INCOME";
const teuerung = (
<NumberField
label="Teuerungsausgleich (%/Jahr)"
help="Indexiert den Betrag ueber die Phasenjahre (Jahr t = Basis x (1+Satz)^(t-1)). Default = Phaseninflation. 0% = nominal flach (real sinkend)."
step={0.1}
value={num(pd.teuerungsausgleich, context.phaseInflation)}
onChange={(v) => setP({ teuerungsausgleich: v })}
/>
);
// Basiswert (erstes Jahr der Phase). Ab Phase 2 mit dem fortgeschriebenen Endwert der
// Vorphase vorbelegt, aber bewusst aenderbar (Teilzeit, Beförderung, Jobwechsel …).
const baseLabel = isIncome ? "Jahreseinkommen (CHF, erstes Jahr)" : "Jahresausgaben (CHF, erstes Jahr)";
// Basiswert (erstes Jahr). Ab Phase 2 mit dem fortgeschriebenen Wert der Vorphase
// vorbelegt, aber bewusst aenderbar (Teilzeit, Befoerderung, Jobwechsel …).
const baseValue = typeof pd.amount === "number" ? pd.amount : carried ? context.derivedStart : 0;
const rate = num(pd.teuerungsausgleich, 0);
const d = context.deflatorStart || 1;
// Info-Gegenwert im ersten Jahr: Einkommen -> real; Ausgaben -> nominal.
const otherValue = isIncome ? Math.round(baseValue / d) : Math.round(baseValue * d);
return (
<>
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
{isIncome
? "Einkommen wird NOMINAL erfasst (die Zahl auf dem Lohnausweis). Der reale Wert (heutige Kaufkraft) wird nur zur Info angezeigt."
: "Ausgaben werden REAL erfasst (in heutiger Kaufkraft). Die Inflation (plan-weit) rechnet daraus automatisch die nominalen Ausgaben nur zur Info."}
</p>
<MoneyField
label={baseLabel}
label={isIncome ? "Jahreseinkommen NOMINAL (erstes Jahr)" : "Jahresausgaben REAL (erstes Jahr, heutige Kaufkraft)"}
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
value={baseValue}
onChange={(v) => setP({ amount: v })}
/>
{teuerung}
<DerivedField
label={isIncome ? "≈ real (heutige Kaufkraft)" : "≈ nominal (in diesem Jahr)"}
value={otherValue}
help="Nur zur Info, wird automatisch berechnet."
/>
<NumberField
label={isIncome ? "Nominale Lohnerhoehung (%/Jahr)" : "Reale Mehrausgaben (%/Jahr)"}
help={
isIncome
? "Geschaetzte jaehrliche Lohnerhoehung. 0% = Lohn bleibt nominal gleich und verliert real an Kaufkraft."
: "Zusaetzliche reale Mehrausgaben pro Jahr (z. B. weil man sich mehr goennt). Die Inflation kommt separat automatisch dazu. 0% = gleicher Lebensstandard."
}
step={0.1}
value={rate}
onChange={(v) => setP({ teuerungsausgleich: v })}
/>
</>
);
}