V4-Rechenmodell: Flow-Indexierung, Cash-Ausgleichstopf, Ruin-Erkennung
Deploy App / deploy (push) Successful in 3m6s
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:
@@ -22,10 +22,9 @@ export interface CellContext {
|
||||
durationYears: number;
|
||||
isRetirementTransition: boolean;
|
||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
||||
carried: boolean; // Phase >= 2: Startwert wird aus der Vorphase fortgeschrieben
|
||||
derivedStart: number; // fortgeschriebener Basis-Startwert (read-only Anzeige)
|
||||
quotaRateMax: number; // Max fuer eine Sparrate/Verzehrrate dieses Elements
|
||||
capitalMax?: number; // Max fuer Startkapital/Neuinvestition (undefined = kein Cap)
|
||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||
phaseInflation: number; // Default fuer Teuerungsausgleich
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -135,9 +134,38 @@ export function ElementPhaseFields({
|
||||
const carried = context.carried;
|
||||
switch (element.category) {
|
||||
case "INCOME":
|
||||
return <MoneyField label="Jahreseinkommen (CHF)" value={num(pd.amount)} onChange={(v) => setP({ amount: v })} />;
|
||||
case "EXPENSE":
|
||||
return <MoneyField label="Jahresausgaben (CHF)" value={num(pd.amount)} onChange={(v) => setP({ amount: v })} />;
|
||||
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 })}
|
||||
/>
|
||||
);
|
||||
if (carried) {
|
||||
return (
|
||||
<>
|
||||
<DerivedField label="Basiswert (fortgeschrieben)" value={context.derivedStart} help="Indexierter Wert aus der Vorphase (Jahr 1 dieser Phase)." />
|
||||
<MoneyField
|
||||
label="Uebersteuern (optional, CHF)"
|
||||
help="Setzt den Basiswert dieser Phase neu (z. B. Einkommensknick bei Teilzeit/Fruehpension). Leer = fortgeschrieben."
|
||||
value={num(pd.amountOverride)}
|
||||
onChange={(v) => setP({ amountOverride: v })}
|
||||
/>
|
||||
{teuerung}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MoneyField label={isIncome ? "Jahreseinkommen (CHF, erstes Jahr)" : "Jahresausgaben (CHF, erstes Jahr)"} value={num(pd.amount)} onChange={(v) => setP({ amount: v })} />
|
||||
{teuerung}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case "AHV":
|
||||
if (!context.ownerWorking) {
|
||||
return (
|
||||
@@ -175,12 +203,12 @@ export function ElementPhaseFields({
|
||||
label="Zusatzeinlage aus Kapital (CHF)"
|
||||
help="Aufstockung aus dem verfuegbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
max={context.capitalMax}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<MoneyField label="Aktueller PK-Wert (CHF)" value={num(pd.currentValue)} max={context.capitalMax} onChange={(v) => setP({ currentValue: v })} />
|
||||
<MoneyField label="Aktueller PK-Wert (CHF)" value={num(pd.currentValue)} onChange={(v) => setP({ currentValue: v })} />
|
||||
)}
|
||||
<MoneyField
|
||||
label="Jaehrliche Einzahlung (CHF)"
|
||||
@@ -204,18 +232,18 @@ export function ElementPhaseFields({
|
||||
label="Zusatzeinlage aus Kapital (CHF)"
|
||||
help="Aufstockung aus dem verfuegbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
max={context.capitalMax}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<MoneyField label="Aktueller 3a-Wert (CHF)" value={num(pd.currentValue)} max={context.capitalMax} onChange={(v) => setP({ currentValue: v })} />
|
||||
<MoneyField label="Aktueller 3a-Wert (CHF)" value={num(pd.currentValue)} onChange={(v) => setP({ currentValue: v })} />
|
||||
)}
|
||||
<MoneyField
|
||||
label="Jaehrliche Einzahlung (CHF)"
|
||||
help={`Maximal CHF ${PILLAR_3A_MAX_ANNUAL.toLocaleString("de-CH")} (2026, mit PK) und hoechstens die Sparquote. Wird von der Sparquote abgezogen.`}
|
||||
value={num(pd.annualContribution)}
|
||||
max={Math.min(PILLAR_3A_MAX_ANNUAL, context.quotaRateMax)}
|
||||
max={PILLAR_3A_MAX_ANNUAL}
|
||||
onChange={(v) => setP({ annualContribution: v })}
|
||||
/>
|
||||
<NumberField label="Erwartete Rendite (%/Jahr)" step={0.1} value={num(pd.expectedReturn)} onChange={(v) => setP({ expectedReturn: v })} />
|
||||
@@ -230,7 +258,7 @@ export function ElementPhaseFields({
|
||||
label="Amortisation (CHF/Jahr)"
|
||||
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote."
|
||||
value={num(pd.amortization)}
|
||||
max={context.quotaRateMax}
|
||||
|
||||
onChange={(v) => setP({ amortization: v })}
|
||||
/>
|
||||
</>
|
||||
@@ -238,13 +266,13 @@ export function ElementPhaseFields({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MoneyField label="Kaufpreis (CHF)" value={num(pd.purchasePrice)} max={context.capitalMax != null ? context.capitalMax + num(pd.mortgage) : undefined} onChange={(v) => setP({ purchasePrice: v })} />
|
||||
<MoneyField label="Kaufpreis (CHF)" value={num(pd.purchasePrice)} onChange={(v) => setP({ purchasePrice: v })} />
|
||||
<MoneyField label="Hypothek (CHF)" value={num(pd.mortgage)} onChange={(v) => setP({ mortgage: v })} />
|
||||
<MoneyField
|
||||
label="Amortisation (CHF/Jahr)"
|
||||
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote."
|
||||
value={num(pd.amortization)}
|
||||
max={context.quotaRateMax}
|
||||
|
||||
onChange={(v) => setP({ amortization: v })}
|
||||
/>
|
||||
</>
|
||||
@@ -259,23 +287,18 @@ export function ElementPhaseFields({
|
||||
label="Zusatzinvestition aus Kapital (CHF)"
|
||||
help="Neuinvestition aus dem verfuegbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
max={context.capitalMax}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<MoneyField label="Startwert (CHF)" value={num(pd.startValue)} max={context.capitalMax} onChange={(v) => setP({ startValue: v })} />
|
||||
<MoneyField label="Startwert (CHF)" value={num(pd.startValue)} onChange={(v) => setP({ startValue: v })} />
|
||||
)}
|
||||
<NumberField label="Erwartete Rendite (%/Jahr)" step={0.1} value={num(pd.expectedReturn)} onChange={(v) => setP({ expectedReturn: v })} />
|
||||
<MoneyField
|
||||
label={context.isConsumption ? "Jaehrliche Bezugsrate (CHF)" : "Jaehrlicher Sparbeitrag (CHF)"}
|
||||
help={
|
||||
context.isConsumption
|
||||
? "In dieser Verzehrphase deckt dieser Betrag die Verzehrquote (max. die Verzehrquote)."
|
||||
: "Wird von der Sparquote abgezogen (max. die Sparquote)."
|
||||
}
|
||||
label="Jaehrlicher Sparbeitrag (CHF)"
|
||||
help="Flacher Jahresbetrag, der ins Vermoegen fliesst und vom Cash abgezogen wird. Zum Entnehmen im Alter das Element im Uebergang verkaufen."
|
||||
value={num(pd.annualContribution)}
|
||||
max={context.quotaRateMax}
|
||||
onChange={(v) => setP({ annualContribution: v })}
|
||||
/>
|
||||
</>
|
||||
@@ -292,7 +315,7 @@ export function ElementPhaseFields({
|
||||
label="Jaehrliche Tilgung (CHF)"
|
||||
help="Zaehlt gegen die Sparquote (max. die Sparquote)."
|
||||
value={num(pd.annualRepayment)}
|
||||
max={context.quotaRateMax}
|
||||
|
||||
onChange={(v) => setP({ annualRepayment: v })}
|
||||
/>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user