UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
Deploy App / deploy (push) Successful in 1m1s
Deploy App / deploy (push) Successful in 1m1s
Rein an der Oberflaeche -- Berechnung, Datenmodell und API-Semantik unveraendert (103 Tests unveraendert gruen). Paket A (Fundament): - durchgehend Du-Form und echte Umlaute in allen sichtbaren Texten, inkl. API-Fehlermeldungen (vorher Mix aus Sie/Du und ae/oe/ue) - neue UI-Primitiven (ui.tsx): Button, Modal mit ESC/Fokus-Falle/Animation, Bestaetigungs-Dialog statt window.confirm, Toasts statt alert, Skeleton-Loader, EmptyState - eigene Attention-Farbe (Amber) fuer offene Entscheide, getrennt vom Akzent - Micro-Interactions mit prefers-reduced-motion-Fallback Paket B (Onboarding, Roadmap Nr. 10): - gefuehrter Plan-Assistent in 5 Schritten; Einkommen bewusst pro Person (raeumt die 9.9-AHV-Falle aus); reine Orchestrierung bestehender Endpunkte - Beispielplan mit einem Klick; Uebergaenge absichtlich offen - interaktive Tour ueber die Planansicht (localStorage, jederzeit neu startbar) - abgeleitete "Naechste Schritte"-Karte (offene Entscheide, fehlende Elemente, fehlende Pensionsphase, Ruin -> Einflussfaktoren) Paket C (Struktur): - Inspector-Panel rechts statt Modals fuer alle Einzel-Bearbeitungen; Matrix bleibt sichtbar, Zellklick wechselt den Inhalt - Phasenkopf auf vier Kern-Infos entschlackt (Rest in der 0.11-Detailansicht) - Matrix mit eigenem Scrollbereich, Koepfe beidachsig fixiert - Sidebar-Gruppen "Meine Plaene" / "Wissen"; "So rechnet FPT" statt SPEZIFIKATION; "Szenario-Profil" statt "Plan-Einstellungen" - Aktions-Icons ohne Hover sichtbar (Touch) Paket D (Extras): - Sparklines je Element-Zeile aus den 0.11-Verlaufswerten - Befehls-Palette (Ctrl/Cmd+K) - Ruin-Banner verlinkt auf die Einflussfaktoren Nebenbei: der ProfileMenu-Lint-Fehler und der Selection-Rest (9.17) sind behoben -- npm run lint laeuft erstmals fehlerfrei. SPEZIFIKATION auf 0.13: neue Kapitel 3.2.8, 3.7.6-3.7.9, 9.23, 9.24; 3.6.3 und 3.7.1 ueberarbeitet, 9.17 bereinigt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+120
-120
@@ -27,13 +27,13 @@ export interface PersonPhaseInfo {
|
||||
}
|
||||
|
||||
// --- Rechenweg-Protokoll (Roadmap Nr. 41) ------------------------------------------------
|
||||
// Ein Schritt haelt die Formel UND die eingesetzten Zahlen fest. Entscheidend: Die Schritte
|
||||
// entstehen INNERHALB der Berechnung, als Nebenprodukt der Rechnung, die ohnehin laeuft.
|
||||
// Damit kann die angezeigte Herleitung nicht von der tatsaechlichen Rechnung abdriften --
|
||||
// eine zweite Formel-Implementierung im UI waere die gefaehrlichste Variante ueberhaupt.
|
||||
// Ein Schritt hält die Formel UND die eingesetzten Zahlen fest. Entscheidend: Die Schritte
|
||||
// entstehen INNERHALB der Berechnung, als Nebenprodukt der Rechnung, die ohnehin läuft.
|
||||
// Damit kann die angezeigte Herleitung nicht von der tatsächlichen Rechnung abdriften --
|
||||
// eine zweite Formel-Implementierung im UI wäre die gefährlichste Variante überhaupt.
|
||||
export interface TraceStep {
|
||||
label: string;
|
||||
formula?: string; // abstrakt, z. B. "Basis x (1 + Lohnerhoehung)^(t-1)"
|
||||
formula?: string; // abstrakt, z. B. "Basis x (1 + Lohnerhöhung)^(t-1)"
|
||||
substituted?: string; // mit eingesetzten Zahlen, z. B. "100'000 x (1 + 2%)^4"
|
||||
result: number;
|
||||
unit?: "CHF" | "%" | "Jahre" | "";
|
||||
@@ -47,15 +47,15 @@ export interface Trace {
|
||||
}
|
||||
|
||||
export interface ComputeOptions {
|
||||
// Standardmaessig aus: die Monte-Carlo-Simulation ruft computePlan zehntausendfach auf
|
||||
// Standardmässig aus: die Monte-Carlo-Simulation ruft computePlan zehntausendfach auf
|
||||
// und darf von der Protokollierung nichts merken.
|
||||
explain?: boolean;
|
||||
}
|
||||
|
||||
// Ein Datenpunkt pro Jahr JE ELEMENT -- Grundlage der Detailansicht (Roadmap Nr. 43).
|
||||
// Nur die Phasengrenzen zu kennen reicht nicht: Eine Anlage mit 5 % ueber 20 Jahre waechst
|
||||
// konvex, eine Gerade von Start zu Ende waere sichtbar falsch (gleiche Ueberlegung wie beim
|
||||
// Vermoegensverlauf in v0.9).
|
||||
// Nur die Phasengrenzen zu kennen reicht nicht: Eine Anlage mit 5 % über 20 Jahre wächst
|
||||
// konvex, eine Gerade von Start zu Ende wäre sichtbar falsch (gleiche Überlegung wie beim
|
||||
// Vermögensverlauf in v0.9).
|
||||
export interface ElementYearPoint {
|
||||
year: number; // 1-basiert ab Planbeginn
|
||||
age: number; // Alter Person A am Jahresende
|
||||
@@ -81,47 +81,47 @@ export interface ElementPhaseComputed {
|
||||
note: string | null;
|
||||
yearly: ElementYearPoint[]; // Verlauf innerhalb dieser Phase
|
||||
trace?: Trace; // Rechenweg der Phasenwerte (nur mit explain)
|
||||
transitionTrace?: Trace; // Rechenweg des Uebergangs NACH dieser Phase (nur mit explain)
|
||||
transitionTrace?: Trace; // Rechenweg des Übergangs NACH dieser Phase (nur mit explain)
|
||||
}
|
||||
|
||||
// --- Wasserfall-Zerlegungen (Roadmap Nr. 43) ---------------------------------------------
|
||||
//
|
||||
// WICHTIG -- der Unterschied zwischen den beiden: Sparraten, Amortisationen und
|
||||
// Zusatzinvestitionen sind UMBUCHUNGEN. Sie verlassen das Cash und erhoehen im selben Zug
|
||||
// einen Vermoegenswert bzw. senken eine Schuld; das Vermoegen bleibt unveraendert. Sie
|
||||
// gehoeren deshalb in die Cash-Bruecke und ausdruecklich NICHT in die Vermoegens-Bruecke --
|
||||
// dort als Abgang gezeichnet wuerden sie eine Vermoegensminderung suggerieren, die es nicht
|
||||
// Zusatzinvestitionen sind UMBUCHUNGEN. Sie verlassen das Cash und erhöhen im selben Zug
|
||||
// einen Vermögenswert bzw. senken eine Schuld; das Vermögen bleibt unverändert. Sie
|
||||
// gehören deshalb in die Cash-Brücke und ausdrücklich NICHT in die Vermögens-Brücke --
|
||||
// dort als Abgang gezeichnet würden sie eine Vermögensminderung suggerieren, die es nicht
|
||||
// gibt.
|
||||
export interface WealthBridge {
|
||||
openingWealth: number; // Endvermoegen der Vorphase (Phase 1: Startvermoegen)
|
||||
// Uebergang IN diese Phase -- echte Vermoegensaenderungen an der Phasengrenze:
|
||||
openingWealth: number; // Endvermögen der Vorphase (Phase 1: Startvermögen)
|
||||
// Übergang IN diese Phase -- echte Vermögensänderungen an der Phasengrenze:
|
||||
oneOffInflow: number; // einmaliger Zufluss (netto nach Steuer)
|
||||
oneOffOutflow: number; // einmalige Kosten
|
||||
transitionTax: number; // Kapitalbezugs- und Grundstueckgewinnsteuer (Abgang)
|
||||
pensionConversion: number; // in eine Rente umgewandeltes PK-Kapital (verlaesst die Bilanz)
|
||||
transitionTax: number; // Kapitalbezugs- und Grundstückgewinnsteuer (Abgang)
|
||||
pensionConversion: number; // in eine Rente umgewandeltes PK-Kapital (verlässt die Bilanz)
|
||||
saleGainLoss: number; // Verkaufspreis minus Verkehrswert bei einem Immobilienverkauf
|
||||
startWealth: number; // Kontrollpunkt = startWealthNominal
|
||||
// Innerhalb der Phase:
|
||||
quotaTotal: number; // Summe (Einkommen - nominale Ausgaben) ueber alle Phasenjahre
|
||||
investmentReturn: number; // Rendite auf PK/3a/Sonstigem Vermoegen
|
||||
quotaTotal: number; // Summe (Einkommen - nominale Ausgaben) über alle Phasenjahre
|
||||
investmentReturn: number; // Rendite auf PK/3a/Sonstigem Vermögen
|
||||
propertyAppreciation: number; // Wertsteigerung der Liegenschaft
|
||||
pensionFundContribution: number; // PK-Beitraege: erhoehen das Vermoegen, ohne Cash zu kosten
|
||||
pensionFundContribution: number; // PK-Beiträge: erhöhen das Vermögen, ohne Cash zu kosten
|
||||
endWealth: number; // = endWealthNominal
|
||||
residual: number; // Rundungsdifferenz (Kontrollgroesse, sollte nahe 0 sein)
|
||||
residual: number; // Rundungsdifferenz (Kontrollgrösse, sollte nahe 0 sein)
|
||||
}
|
||||
|
||||
export interface CashBridge {
|
||||
openingCash: number; // Cash-Endbestand der Vorphase (Phase 1: initialCash)
|
||||
capitalInflow: number; // Verkaeufe + PK-/3a-Bezuege aus dem Uebergang
|
||||
capitalInflow: number; // Verkäufe + PK-/3a-Bezüge aus dem Übergang
|
||||
oneOffInflow: number;
|
||||
immediateRepay: number; // Sofort-Tilgungen und Sonderamortisation (Abgang)
|
||||
oneOffOutflow: number;
|
||||
investments: number; // Zusatz-/Neuinvestitionen am Phasenanfang (Abgang)
|
||||
cashStart: number; // Kontrollpunkt
|
||||
quotaTotal: number;
|
||||
savingRates: number; // 3a + Sparbeitraege (Abgang)
|
||||
savingRates: number; // 3a + Sparbeiträge (Abgang)
|
||||
debtRates: number; // Amortisationen + Tilgungen (Abgang)
|
||||
withdrawals: number; // Bezugsraten aus Sonstigem Vermoegen (Zugang)
|
||||
withdrawals: number; // Bezugsraten aus Sonstigem Vermögen (Zugang)
|
||||
cashEnd: number;
|
||||
residual: number;
|
||||
}
|
||||
@@ -143,15 +143,15 @@ export interface PhaseComputed {
|
||||
quotaStart: number;
|
||||
quotaEnd: number;
|
||||
isConsumption: boolean;
|
||||
// Geplante Sparrate im ERSTEN Phasenjahr: 3a + Sonstiges-Vermoegen-Sparbeitrag + Amort. +
|
||||
// Geplante Sparrate im ERSTEN Phasenjahr: 3a + Sonstiges-Vermögen-Sparbeitrag + Amort. +
|
||||
// Tilgung. Amortisation/Tilgung entfallen, sobald Hypothek/Schuld abbezahlt sind -- die Rate
|
||||
// kann in spaeteren Phasenjahren also tiefer liegen.
|
||||
// kann in späteren Phasenjahren also tiefer liegen.
|
||||
plannedSaveRate: number;
|
||||
plannedWithdrawRate: number; // geplante Verzehrrate: Bezugsraten aus Sonstigem Vermoegen
|
||||
capitalInflow: number; // Kapitalzufluss: PK-/3a-Bezuege + Verkaeufe (aus dem Uebergang in diese Phase)
|
||||
plannedWithdrawRate: number; // geplante Verzehrrate: Bezugsraten aus Sonstigem Vermögen
|
||||
capitalInflow: number; // Kapitalzufluss: PK-/3a-Bezüge + Verkäufe (aus dem Übergang in diese Phase)
|
||||
capitalInvest: number; // Kapitalinvestitionen: Zusatz-/Neuinvestitionen + sofortige Tilgungen
|
||||
// Einmalige Sonderein-/ausgaben aus dem Uebergang in DIESE Phase (nominal, netto nach Steuer).
|
||||
// Bewusst getrennt von capitalInflow/capitalInvest: eine Erbschaft ist kein Verkaufserloes,
|
||||
// Einmalige Sonderein-/ausgaben aus dem Übergang in DIESE Phase (nominal, netto nach Steuer).
|
||||
// Bewusst getrennt von capitalInflow/capitalInvest: eine Erbschaft ist kein Verkaufserlös,
|
||||
// ein Poolbau keine Kapitalinvestition.
|
||||
oneOffInflow: number;
|
||||
oneOffInflowLabel: string | null;
|
||||
@@ -159,29 +159,29 @@ export interface PhaseComputed {
|
||||
oneOffOutflowLabel: string | null;
|
||||
cashStart: number;
|
||||
cashEnd: number;
|
||||
cashNegative: boolean; // Cash faellt in dieser Phase (irgendwann) unter 0 -> Liquiditaetsluecke
|
||||
incomplete: boolean; // roter Status = Liquiditaetsluecke
|
||||
cashNegative: boolean; // Cash fällt in dieser Phase (irgendwann) unter 0 -> Liquiditätslücke
|
||||
incomplete: boolean; // roter Status = Liquiditätslücke
|
||||
elements: ElementPhaseComputed[];
|
||||
startWealthNominal: number; // inkl. Cash
|
||||
endWealthNominal: number; // inkl. Cash
|
||||
cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Bestandswerte)
|
||||
cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Bestandswerte)
|
||||
flowDeflatorEnd: number; // Deflator fuer den Flow-Endwert (Jahr `duration`)
|
||||
flowDeflatorEnd: number; // Deflator für den Flow-Endwert (Jahr `duration`)
|
||||
endWealthReal: number;
|
||||
wealthBridge: WealthBridge;
|
||||
cashBridge: CashBridge;
|
||||
traces?: Trace[]; // Rechenwege der Phasen-Kennzahlen (nur mit explain)
|
||||
}
|
||||
|
||||
// Ein Datenpunkt pro Jahr (ueber alle Phasen), fuer die Verlaufsgrafik.
|
||||
// Ein Datenpunkt pro Jahr (über alle Phasen), für die Verlaufsgrafik.
|
||||
export interface YearPoint {
|
||||
year: number; // 1-basiert ab Planbeginn
|
||||
age: number; // Alter Person A am Jahresende
|
||||
income: number; // Einkommen inkl. Renten (nominal)
|
||||
expenseNominal: number;
|
||||
expenseReal: number;
|
||||
// Gesamtvermoegen am Jahresende (inkl. Cash, abzgl. Schulden) -- fuer den
|
||||
// Vermoegensverlauf ueber ALLE Jahre statt nur ueber die Phasengrenzen.
|
||||
// Gesamtvermögen am Jahresende (inkl. Cash, abzgl. Schulden) -- für den
|
||||
// Vermögensverlauf über ALLE Jahre statt nur über die Phasengrenzen.
|
||||
wealthNominal: number;
|
||||
wealthReal: number;
|
||||
}
|
||||
@@ -190,8 +190,8 @@ export interface PlanComputed {
|
||||
phases: PhaseComputed[];
|
||||
yearly: YearPoint[];
|
||||
nachlass: number;
|
||||
ruinAge: number | null; // Alter (Person A), in dem das Gesamtvermoegen (inkl. Cash) erstmals < 0 faellt
|
||||
ahvCareer: AhvCareer[]; // Beitragskarriere je Person (fuer die AHV-Pruefung am Uebergang)
|
||||
ruinAge: number | null; // Alter (Person A), in dem das Gesamtvermögen (inkl. Cash) erstmals < 0 fällt
|
||||
ahvCareer: AhvCareer[]; // Beitragskarriere je Person (für die AHV-Prüfung am Übergang)
|
||||
traces?: Trace[]; // plan-weite Rechenwege: Deflatoren, AHV-Karriere, Ruinalter (nur mit explain)
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export interface PlanComputed {
|
||||
// mdJE >= 72 x R0 -> Maximalrente 2 x R0
|
||||
// Verifiziert gegen die amtliche Tabelle 318.117.1 (51/51 Zeilen exakt, siehe Tests).
|
||||
// Bewusst STETIG gerechnet: die amtliche Tabelle stuft das mdJE auf Vielfache von 1.2 x R0;
|
||||
// fuer eine Planung ist der stetige Wert naeher an der Wahrheit (Abweichung < 20/Monat).
|
||||
// für eine Planung ist der stetige Wert näher an der Wahrheit (Abweichung < 20/Monat).
|
||||
export function ahvMonthlyFullPension(mdJE: number): number {
|
||||
const r0 = AHV_MIN_MONTHLY_FULL;
|
||||
if (mdJE <= 12 * r0) return r0;
|
||||
@@ -212,20 +212,20 @@ export function ahvMonthlyFullPension(mdJE: number): number {
|
||||
return mdJE <= 36 * r0 ? r0 * (0.74 + 0.26 * x) : r0 * (1.04 + 0.16 * x);
|
||||
}
|
||||
|
||||
// Beitragskarriere einer Person fuer die AHV -- akkumuliert ueber die Erwerbsphasen des Plans.
|
||||
// Beitragskarriere einer Person für die AHV -- akkumuliert über die Erwerbsphasen des Plans.
|
||||
// ACHTUNG: Alle Einkommen sind BRUTTO. Das Tool erfasst netto (so stimmt der Cash-Fluss), die
|
||||
// AHV bemisst sich aber am Bruttolohn -- die Umrechnung passiert beim Aufbau der Karriere.
|
||||
export interface AhvCareer {
|
||||
personId: string;
|
||||
role: PersonRole;
|
||||
plannedAvgGrossIncome: number; // reales BRUTTO-Durchschnittseinkommen der Beitragsjahre im Plan
|
||||
planYears: number; // Beitragsjahre im Plan (Dauer abzueglich Ausfalljahre)
|
||||
planYears: number; // Beitragsjahre im Plan (Dauer abzüglich Ausfalljahre)
|
||||
yearsBeforePlan: number; // Jahre zwischen Alter 21 und Planbeginn
|
||||
gapYearsInPlan: number;
|
||||
}
|
||||
|
||||
// Massgebendes durchschnittliches Jahreseinkommen: gewichteter Mittelwert der realen
|
||||
// Einkommen ueber ALLE Beitragsjahre (vor Planbeginn + im Plan). REAL gerechnet, weil die
|
||||
// Einkommen über ALLE Beitragsjahre (vor Planbeginn + im Plan). REAL gerechnet, weil die
|
||||
// AHV vergangene Einkommen aufwertet UND die Schwellen indexiert -- beides hebt sich in
|
||||
// realer Betrachtung weitgehend auf.
|
||||
// `avgGrossIncomeBefore` ist -- wie die Karriere -- ein BRUTTO-Wert.
|
||||
@@ -236,7 +236,7 @@ export function ahvMdje(career: AhvCareer, avgGrossIncomeBefore: number, gapYear
|
||||
return (avgGrossIncomeBefore * yearsBefore + career.plannedAvgGrossIncome * career.planYears) / totalYears;
|
||||
}
|
||||
|
||||
// Jaehrliche AHV-Rente: Vollrente zum mdJE, mal 13 Zahlungen, gekuerzt um die Ausfalljahre
|
||||
// Jährliche AHV-Rente: Vollrente zum mdJE, mal 13 Zahlungen, gekürzt um die Ausfalljahre
|
||||
// (Rentenskala: pro fehlendes Beitragsjahr 1/44).
|
||||
export function ahvAnnualPension(mdJE: number, totalGapYears: number): number {
|
||||
const factor = Math.max(
|
||||
@@ -251,7 +251,7 @@ export function ahvYearsBeforePlan(ageAtPlanStart: number): number {
|
||||
return Math.max(0, ageAtPlanStart - AHV_CONTRIBUTION_START_AGE);
|
||||
}
|
||||
|
||||
// Maximale Dauer einer neuen Phase bis zum naechsten Pensionsereignis (null = unbegrenzt).
|
||||
// Maximale Dauer einer neuen Phase bis zum nächsten Pensionsereignis (null = unbegrenzt).
|
||||
export function maxPhaseDuration(
|
||||
persons: { role: PersonRole; age: number; retirementAge: number }[],
|
||||
yearsBefore: number
|
||||
@@ -266,13 +266,13 @@ export function maxPhaseDuration(
|
||||
|
||||
interface Carry {
|
||||
status: ElementStatus;
|
||||
value: number; // Aktiven-Saldo (PK/3a/Sonstiges Vermoegen) am Ende der Vorphase
|
||||
value: number; // Aktiven-Saldo (PK/3a/Sonstiges Vermögen) am Ende der Vorphase
|
||||
mortgage: number; // Immobilie: Resthypothek
|
||||
propertyValue: number; // Immobilie: Verkehrswert am Ende der Vorphase (inkl. Wertsteigerung)
|
||||
propertyPurchase: number; // Immobilie: urspruenglicher Kaufpreis (Basis der Grundstueckgewinnsteuer)
|
||||
propertyPurchase: number; // Immobilie: ursprünglicher Kaufpreis (Basis der Grundstückgewinnsteuer)
|
||||
owed: number; // Schulden: Restschuld (positiv)
|
||||
pkPensionAnnual: number; // PK: jaehrliche Rente nach Verrentung
|
||||
flowBasis: number; // Einkommen/Ausgaben: indexierter Basiswert der naechsten Phase
|
||||
pkPensionAnnual: number; // PK: jährliche Rente nach Verrentung
|
||||
flowBasis: number; // Einkommen/Ausgaben: indexierter Basiswert der nächsten Phase
|
||||
hasCarry: boolean;
|
||||
}
|
||||
|
||||
@@ -296,12 +296,12 @@ function fmt(v: number): string {
|
||||
return sign + Math.abs(rounded).toString().replace(/\B(?=(\d{3})+(?!\d))/g, "'");
|
||||
}
|
||||
|
||||
// Generisch, damit der Aufrufer den vollen Personen-Typ (inkl. age) behaelt.
|
||||
// Generisch, damit der Aufrufer den vollen Personen-Typ (inkl. age) behält.
|
||||
function personByRole<T extends { role: PersonRole }>(persons: T[], role: string): T | null {
|
||||
return persons.find((p) => p.role === role) ?? null;
|
||||
}
|
||||
|
||||
// Ein Zufalls-Szenario fuer die Monte-Carlo-Simulation: liefert je Jahr eine Inflation und
|
||||
// Ein Zufalls-Szenario für die Monte-Carlo-Simulation: liefert je Jahr eine Inflation und
|
||||
// je Element/Jahr eine Rendite. Ohne Sample rechnet computePlan rein deterministisch (die
|
||||
// geplanten Annahmen), mit Sample einen einzelnen simulierten Pfad. Jahr ist 1-basiert
|
||||
// (ab Planbeginn); der Inflations-Index ist 0-basiert (inflation[0] = Jahr 1).
|
||||
@@ -310,7 +310,7 @@ export interface PlanSample {
|
||||
assetReturn: (elementId: string, year: number) => number;
|
||||
}
|
||||
|
||||
// Kurzform fuer einen Rechenschritt. `pct` formatiert Prozentsaetze lesbar (2 statt 2.0000001).
|
||||
// Kurzform für einen Rechenschritt. `pct` formatiert Prozentsätze lesbar (2 statt 2.0000001).
|
||||
function st(
|
||||
label: string,
|
||||
result: number,
|
||||
@@ -336,7 +336,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
|
||||
// Kumulierter Inflations-Deflator je Jahr (cumInfl[0] = 1, cumInfl[k] = Kaufkraftfaktor nach
|
||||
// k Jahren). Deterministisch identisch zur bisherigen (1+infl)^k-Formel; mit Sample variiert
|
||||
// die Inflation pro Jahr. Ersetzt die frueheren geschlossenen Potenz-Ausdruecke.
|
||||
// die Inflation pro Jahr. Ersetzt die früheren geschlossenen Potenz-Ausdrücke.
|
||||
const totalYears = phases.reduce((s, p) => s + Math.max(1, p.durationYears), 0);
|
||||
const inflationOfYear = (year: number) =>
|
||||
sample ? sample.inflation[year - 1] ?? plan.inflationRateDefault : plan.inflationRateDefault;
|
||||
@@ -347,7 +347,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
// AHV-Beitragskarriere je Person: reales Einkommen x Beitragsjahre, und Beitragsjahre.
|
||||
const ahvIncomeAccum = new Map<string, number>();
|
||||
const ahvYearsAccum = new Map<string, number>();
|
||||
// Karriere VOR Planbeginn -- aus der Pruefung am Pensions-Uebergang bzw. (fuer bereits
|
||||
// Karriere VOR Planbeginn -- aus der Prüfung am Pensions-Übergang bzw. (für bereits
|
||||
// Pensionierte) aus der Phasenzelle der ersten Phase.
|
||||
const ahvBeforeByPerson = new Map<string, { avg: number; gap: number }>();
|
||||
const carries = new Map<string, Carry>();
|
||||
@@ -358,18 +358,18 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let yearsBefore = 0;
|
||||
let cumulativeInflation = 1;
|
||||
let cashCarryIn = Math.round(plan.initialCash || 0);
|
||||
// Aus dem Uebergang der Vorphase in DIESE Phase fliessende Groessen (Kopf-Kennzahlen).
|
||||
let incomingInflow = 0; // Brutto-Zufluss: Verkaeufe + PK-/3a-Bezuege
|
||||
// Aus dem Übergang der Vorphase in DIESE Phase fliessende Grössen (Kopf-Kennzahlen).
|
||||
let incomingInflow = 0; // Brutto-Zufluss: Verkäufe + PK-/3a-Bezüge
|
||||
let incomingImmediateRepay = 0; // sofortige Schuldentilgungen (Abfluss)
|
||||
let incomingOneOffInflow = 0; // einmaliger Sonderzufluss (netto nach Steuer)
|
||||
let incomingOneOffInflowLabel: string | null = null;
|
||||
let incomingOneOffOutflow = 0; // einmalige Sonderkosten (nominal)
|
||||
let incomingOneOffOutflowLabel: string | null = null;
|
||||
// Echte Vermoegensaenderungen am Uebergang IN die Folgephase (fuer die Vermoegens-Bruecke).
|
||||
let incomingTax = 0; // Kapitalbezugs- + Grundstueckgewinnsteuer
|
||||
// Echte Vermögensänderungen am Übergang IN die Folgephase (für die Vermögens-Brücke).
|
||||
let incomingTax = 0; // Kapitalbezugs- + Grundstückgewinnsteuer
|
||||
let incomingPensionConversion = 0; // in eine Rente umgewandeltes PK-Kapital
|
||||
let incomingSaleGainLoss = 0; // Verkaufspreis minus Verkehrswert
|
||||
let previousEndWealth = 0; // Endvermoegen der Vorphase (Startpunkt der Bruecke)
|
||||
let previousEndWealth = 0; // Endvermögen der Vorphase (Startpunkt der Brücke)
|
||||
let previousCashEnd = Math.round(plan.initialCash || 0);
|
||||
let ruinAge: number | null = null;
|
||||
|
||||
@@ -402,7 +402,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const maxDurationYears = capsFromWorking.length > 0 ? Math.min(...capsFromWorking) : null;
|
||||
const workingByPerson = new Map(personInfos.map((p) => [p.personId, p.working]));
|
||||
|
||||
// Ausfalljahre kumulieren (nur waehrend der Erwerbstaetigkeit).
|
||||
// Ausfalljahre kumulieren (nur während der Erwerbstätigkeit).
|
||||
const gapThisPhase = new Map<string, number>();
|
||||
for (const e of plan.elements) {
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
@@ -413,13 +413,13 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
gapYearsByPerson.set(owner.id, (gapYearsByPerson.get(owner.id) ?? 0) + gy);
|
||||
}
|
||||
|
||||
// Bereits bei Planbeginn pensioniert: es gibt keinen Pensions-Uebergang, an dem die
|
||||
// Beitragskarriere geprueft werden koennte -- die Werte liegen dann in der Phasenzelle.
|
||||
// Bereits bei Planbeginn pensioniert: es gibt keinen Pensions-Übergang, an dem die
|
||||
// Beitragskarriere geprüft werden könnte -- die Werte liegen dann in der Phasenzelle.
|
||||
for (const e of plan.elements) {
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
const owner = personByRole(persons, e.ownerRole);
|
||||
if (!owner || workingByPerson.get(owner.id)) continue;
|
||||
if (ahvBeforeByPerson.has(owner.id)) continue; // aus dem Uebergang bereits gesetzt
|
||||
if (ahvBeforeByPerson.has(owner.id)) continue; // aus dem Übergang bereits gesetzt
|
||||
const pd = e.phaseValues[phase.id] ?? {};
|
||||
ahvBeforeByPerson.set(owner.id, {
|
||||
avg: num(pd.avgIncomeBefore),
|
||||
@@ -427,7 +427,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
});
|
||||
}
|
||||
|
||||
// AHV-Renten der Pensionierten: Vollrente zum mdJE, gekuerzt um die Ausfalljahre.
|
||||
// AHV-Renten der Pensionierten: Vollrente zum mdJE, gekürzt um die Ausfalljahre.
|
||||
const ahvUncapped = new Map<string, number>();
|
||||
for (const e of plan.elements) {
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
@@ -446,16 +446,16 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (sum > cap && sum > 0) for (const [pid, v] of ahvUncapped) ahvFinal.set(pid, Math.round(v * (cap / sum)));
|
||||
}
|
||||
|
||||
// --- Element-Laufzeitzustaende aufbauen ---
|
||||
// --- Element-Laufzeitzustände aufbauen ---
|
||||
const orderedElements = [...plan.elements].sort((a, b) => a.orderIndex - b.orderIndex);
|
||||
const ecById = new Map<string, ElementPhaseComputed>();
|
||||
// Reales Durchschnittseinkommen dieser Phase je Person (fuer die AHV-Karriere).
|
||||
// Reales Durchschnittseinkommen dieser Phase je Person (für die AHV-Karriere).
|
||||
const phaseRealIncomeByPerson = new Map<string, number>();
|
||||
const incomes: { basis: number; idx: number; ec: ElementPhaseComputed }[] = [];
|
||||
const expenses: { basis: number; idx: number; ec: ElementPhaseComputed }[] = [];
|
||||
let renteTotal = 0; // AHV + PK-Renten (nominal fix)
|
||||
// `isPk` fuer die Vermoegens-Bruecke: PK-Beitraege verlassen das Cash NICHT (sie sind im
|
||||
// Nettolohn bereits abgezogen), erhoehen aber das Vermoegen -- sie sind also ein echter
|
||||
// `isPk` für die Vermögens-Brücke: PK-Beiträge verlassen das Cash NICHT (sie sind im
|
||||
// Nettolohn bereits abgezogen), erhöhen aber das Vermögen -- sie sind also ein echter
|
||||
// Zugang und keine Umbuchung.
|
||||
const assets: {
|
||||
value: number;
|
||||
@@ -467,7 +467,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
}[] = [];
|
||||
// mortgage/owed sind LAUFENDE Salden: sie werden in der Jahresschleife abgebaut und am
|
||||
// Nullpunkt gestoppt (keine Rate mehr, sobald abbezahlt). `value` ist der Verkehrswert der
|
||||
// Liegenschaft (waechst mit valueGrowth), `purchase` der urspruengliche Kaufpreis.
|
||||
// Liegenschaft (wächst mit valueGrowth), `purchase` der ursprüngliche Kaufpreis.
|
||||
const realEstates: {
|
||||
value: number;
|
||||
purchase: number;
|
||||
@@ -479,8 +479,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
ec: ElementPhaseComputed;
|
||||
}[] = [];
|
||||
const debts: { owed: number; repay: number; ec: ElementPhaseComputed }[] = [];
|
||||
let fixedRatesTotal = 0; // Sparraten mit konstantem Jahresbetrag: 3a + Sonstiges Vermoegen
|
||||
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermoegen
|
||||
let fixedRatesTotal = 0; // Sparraten mit konstantem Jahresbetrag: 3a + Sonstiges Vermögen
|
||||
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermögen
|
||||
let investmentsFromCash = 0; // Neuinvestitionen/Aufstockungen (ab Phase 2, aus Cash)
|
||||
let wealthStart = 0;
|
||||
let wealthEnd = 0; // wird nach der Jahresschleife gefuellt
|
||||
@@ -524,10 +524,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
switch (e.category) {
|
||||
case "INCOME":
|
||||
case "EXPENSE": {
|
||||
// Einkommen: Basis + Rate sind NOMINAL (Lohnerhoehung). Ausgaben: Basis + Rate sind
|
||||
// Einkommen: Basis + Rate sind NOMINAL (Lohnerhöhung). Ausgaben: Basis + Rate sind
|
||||
// REAL (heutige Kaufkraft; reale Mehrausgaben) -- die Inflation kommt separat dazu.
|
||||
// Rate-Default = 0 %. Basiswert ab Phase 2 = fortgeschriebener Wert der Vorphase
|
||||
// (nominal fuer Einkommen, real fuer Ausgaben), ausser bewusst geaendert (pd.amount).
|
||||
// (nominal für Einkommen, real für Ausgaben), ausser bewusst geändert (pd.amount).
|
||||
const idx = num(pd.teuerungsausgleich, 0);
|
||||
ec.baseValue = carry.hasCarry ? Math.round(carry.flowBasis) : Math.round(num(pd.amount));
|
||||
const basis = !carry.hasCarry
|
||||
@@ -537,15 +537,15 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
: ec.baseValue;
|
||||
(e.category === "INCOME" ? incomes : expenses).push({ basis, idx, ec });
|
||||
|
||||
// AHV: reales Erwerbseinkommen der Person mitfuehren. Nur Einkommen, die einer
|
||||
// Person zugeordnet sind -- bei einem Einzelplan zaehlt "Gemeinsam" zur Person A.
|
||||
// AHV: reales Erwerbseinkommen der Person mitführen. Nur Einkommen, die einer
|
||||
// Person zugeordnet sind -- bei einem Einzelplan zählt "Gemeinsam" zur Person A.
|
||||
// Das Feld ist NETTO erfasst; die AHV bemisst sich am Bruttolohn -> hochrechnen.
|
||||
if (e.category === "INCOME" && ownerWorking) {
|
||||
const attributed =
|
||||
owner ?? (plan.householdType === "SINGLE" && e.ownerRole === "HOUSEHOLD" ? personA : null);
|
||||
if (attributed) {
|
||||
// Die AHV-Karriere ist eine Real-Groesse auf Planungsbasis -- bewusst mit der
|
||||
// festen Plan-Inflation, nicht der (evtl. gewuerfelten) Sample-Inflation.
|
||||
// Die AHV-Karriere ist eine Real-Grösse auf Planungsbasis -- bewusst mit der
|
||||
// festen Plan-Inflation, nicht der (evtl. gewürfelten) Sample-Inflation.
|
||||
const avgRealGross =
|
||||
avgRealFlow(basis, idx, plan.inflationRateDefault, duration, cumInflStart) * AHV_GROSS_FROM_NET_FACTOR;
|
||||
phaseRealIncomeByPerson.set(
|
||||
@@ -555,7 +555,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
}
|
||||
}
|
||||
|
||||
// Basiswert der Folgephase fortschreiben (nominal fuer Einkommen, real fuer Ausgaben).
|
||||
// Basiswert der Folgephase fortschreiben (nominal für Einkommen, real für Ausgaben).
|
||||
carry.flowBasis = basis * Math.pow(1 + idx / 100, duration);
|
||||
break;
|
||||
}
|
||||
@@ -579,13 +579,13 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
ec.endValue = carry.pkPensionAnnual;
|
||||
ec.summary = `Rente ${fmt(carry.pkPensionAnnual)}`;
|
||||
} else if (!ownerWorking) {
|
||||
ec.note = "Vollstaendig bezogen";
|
||||
ec.note = "Vollständig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
} else {
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
const start = base + topUp;
|
||||
const rate = Math.round(num(pd.annualContribution)); // PK-Beitrag zaehlt NICHT zur Quote
|
||||
const rate = Math.round(num(pd.annualContribution)); // PK-Beitrag zählt NICHT zur Quote
|
||||
if (!isFirstPhase) investmentsFromCash += topUp;
|
||||
ec.baseValue = base;
|
||||
ec.startValue = start;
|
||||
@@ -596,7 +596,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
}
|
||||
case "PILLAR_3A": {
|
||||
if (!ownerWorking) {
|
||||
ec.note = "Vollstaendig bezogen";
|
||||
ec.note = "Vollständig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
} else {
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
@@ -628,8 +628,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
break;
|
||||
}
|
||||
case "REAL_ESTATE": {
|
||||
// Urspruenglicher Kaufpreis (Basis der Grundstueckgewinnsteuer) und Verkehrswert
|
||||
// (waechst mit der Wertsteigerung) laufen getrennt.
|
||||
// Ursprünglicher Kaufpreis (Basis der Grundstückgewinnsteuer) und Verkehrswert
|
||||
// (wächst mit der Wertsteigerung) laufen getrennt.
|
||||
const purchase = carry.hasCarry ? carry.propertyPurchase : Math.round(num(pd.purchasePrice));
|
||||
const valueStart = carry.hasCarry ? carry.propertyValue : Math.round(num(pd.purchasePrice));
|
||||
const mortgageStart = carry.hasCarry ? carry.mortgage : Math.round(num(pd.mortgage));
|
||||
@@ -647,7 +647,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
amort,
|
||||
growth: num(pd.valueGrowth),
|
||||
interestRate: num(pd.interestRate),
|
||||
// Default INCLUDED: bestehende Plaene haben die Zinsen in den Ausgaben -> nicht
|
||||
// Default INCLUDED: bestehende Pläne haben die Zinsen in den Ausgaben -> nicht
|
||||
// nochmals abziehen. Nur bei bewusstem "ADD" rechnet das Tool sie dazu.
|
||||
addInterest: pd.interestHandling === "ADD",
|
||||
ec,
|
||||
@@ -677,9 +677,9 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
ahvYearsAccum.set(p.id, (ahvYearsAccum.get(p.id) ?? 0) + contribYears);
|
||||
}
|
||||
|
||||
// --- Jahr-fuer-Jahr: indexierte Flows, Cash-Ausgleich, Verzinsung, Ruin ---
|
||||
// --- Jahr-für-Jahr: indexierte Flows, Cash-Ausgleich, Verzinsung, Ruin ---
|
||||
// Investitionen dieser Phase werden am Phasenanfang abgezogen -> der Cash-Startwert
|
||||
// zeigt den Bestand NACH den Investitionen (kein Doppelzaehlen mit dem Vermoegen).
|
||||
// zeigt den Bestand NACH den Investitionen (kein Doppelzählen mit dem Vermögen).
|
||||
let cash = cashCarryIn - (isFirstPhase ? 0 : investmentsFromCash);
|
||||
const cashStart = cash;
|
||||
let cashNegative = cash < 0;
|
||||
@@ -689,8 +689,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let expenseEnd = 0;
|
||||
let quotaStart = 0;
|
||||
let quotaEnd = 0;
|
||||
let plannedSaveRate = 0; // Kopf-Kennzahl: die tatsaechliche Sparrate im ersten Phasenjahr
|
||||
// Summanden der beiden Wasserfall-Bruecken (ueber alle Phasenjahre kumuliert).
|
||||
let plannedSaveRate = 0; // Kopf-Kennzahl: die tatsächliche Sparrate im ersten Phasenjahr
|
||||
// Summanden der beiden Wasserfall-Brücken (über alle Phasenjahre kumuliert).
|
||||
let quotaTotal = 0;
|
||||
let investmentReturnTotal = 0;
|
||||
let propertyAppreciationTotal = 0;
|
||||
@@ -700,7 +700,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let withdrawalsTotal = 0;
|
||||
|
||||
for (let t = 1; t <= duration; t++) {
|
||||
// Einkommen: nominal (Basis x (1+Lohnerhoehung)^(t-1)) + Renten (nominal fix).
|
||||
// Einkommen: nominal (Basis x (1+Lohnerhöhung)^(t-1)) + Renten (nominal fix).
|
||||
let incomeFlow = renteTotal;
|
||||
for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1);
|
||||
// Ausgaben: real (Basis x (1+reale Mehrausgabe)^(t-1)); nominal = real x kumul. Inflation.
|
||||
@@ -709,7 +709,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
for (const exp of expenses) expenseRealBase += exp.basis * Math.pow(1 + exp.idx / 100, t - 1);
|
||||
|
||||
// Hypothekarzins: NOMINAL aus der Restschuld zu Jahresbeginn -- nicht mit der Inflation
|
||||
// hochrechnen. Zaehlt zu den Ausgaben (und damit in die Quote), sofern nicht bereits
|
||||
// hochrechnen. Zählt zu den Ausgaben (und damit in die Quote), sofern nicht bereits
|
||||
// im Ausgaben-Element enthalten.
|
||||
let interestNominal = 0;
|
||||
for (const re of realEstates) {
|
||||
@@ -721,8 +721,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const expenseReal = expenseRealBase + interestNominal / (inflFactor || 1);
|
||||
const quote = incomeFlow - expenseNominal;
|
||||
|
||||
// Der Vermoegenswert wird erst nach Verzinsung/Cash-Fortschreibung bekannt und weiter
|
||||
// unten nachgetragen (gleiche Groesse, die auch die Ruin-Erkennung verwendet).
|
||||
// Der Vermögenswert wird erst nach Verzinsung/Cash-Fortschreibung bekannt und weiter
|
||||
// unten nachgetragen (gleiche Grösse, die auch die Ruin-Erkennung verwendet).
|
||||
const yearPoint: YearPoint = {
|
||||
year: yearsBefore + t,
|
||||
age: personA.age + yearsBefore + t,
|
||||
@@ -745,11 +745,11 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
quotaEnd = quote;
|
||||
}
|
||||
|
||||
// Vermoegen verzinsen + Sparbeitrag; Bezugsrate entnehmen (gekappt am Bestand) und ins Cash.
|
||||
// Vermögen verzinsen + Sparbeitrag; Bezugsrate entnehmen (gekappt am Bestand) und ins Cash.
|
||||
let cashFromWithdraw = 0;
|
||||
for (const a of assets) {
|
||||
const r = sample ? sample.assetReturn(a.ec.elementId, yearsBefore + t) : a.r;
|
||||
// Arithmetik unveraendert lassen; die Rendite wird als Differenz abgeleitet, damit
|
||||
// Arithmetik unverändert lassen; die Rendite wird als Differenz abgeleitet, damit
|
||||
// die Protokollierung das Ergebnis nicht (per Gleitkomma-Umformung) verschiebt.
|
||||
const grown = a.value * (1 + r / 100) + a.rate;
|
||||
const growth = grown - a.rate - a.value;
|
||||
@@ -761,7 +761,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
}
|
||||
|
||||
// Amortisation/Tilgung: nur so lange und so viel, wie noch Restschuld besteht. Ist die
|
||||
// Hypothek/Schuld abbezahlt, entfaellt die Rate -- sie belastet weder Cash noch Sparquote.
|
||||
// Hypothek/Schuld abbezahlt, entfällt die Rate -- sie belastet weder Cash noch Sparquote.
|
||||
let debtRates = 0;
|
||||
for (const re of realEstates) {
|
||||
const pay = Math.min(re.amort, re.mortgage);
|
||||
@@ -771,7 +771,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
// Hebel: 1 % von 1 Mio sind 10'000, also 10 % eines Eigenkapitals von 100'000.
|
||||
const g = sample ? sample.assetReturn(re.ec.elementId, yearsBefore + t) : re.growth;
|
||||
const valueBefore = re.value;
|
||||
re.value *= 1 + g / 100; // Arithmetik unveraendert
|
||||
re.value *= 1 + g / 100; // Arithmetik unverändert
|
||||
propertyAppreciationTotal += re.value - valueBefore;
|
||||
}
|
||||
for (const d of debts) {
|
||||
@@ -790,7 +790,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
withdrawalsTotal += cashFromWithdraw;
|
||||
|
||||
// Verlaufspunkt je Element (Detailansicht). Bewusst NACH Verzinsung und Tilgung, also
|
||||
// der Stand am Jahresende -- konsistent zum Gesamtvermoegen im YearPoint.
|
||||
// der Stand am Jahresende -- konsistent zum Gesamtvermögen im YearPoint.
|
||||
const age = personA.age + yearsBefore + t;
|
||||
const yr = yearsBefore + t;
|
||||
for (const inc of incomes) {
|
||||
@@ -821,7 +821,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
}
|
||||
}
|
||||
|
||||
// Gesamtvermoegen zum Jahresende t (fuer Ruin-Erkennung).
|
||||
// Gesamtvermögen zum Jahresende t (für Ruin-Erkennung).
|
||||
let total = cash;
|
||||
for (const a of assets) total += a.value;
|
||||
for (const re of realEstates) total += re.value - re.mortgage;
|
||||
@@ -831,7 +831,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
|
||||
}
|
||||
|
||||
// Flow-Deflator fuer den Endwert (Jahr `duration`): eine Kaufkraft-Stufe weniger als der
|
||||
// Flow-Deflator für den Endwert (Jahr `duration`): eine Kaufkraft-Stufe weniger als der
|
||||
// Bestands-Deflator am Phasenende.
|
||||
const flowDeflatorEnd = cumInfl[yearsBefore + duration - 1];
|
||||
|
||||
@@ -1007,7 +1007,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
};
|
||||
}
|
||||
|
||||
// AHV-/PK-Renten: der Rechenweg der Rentenhoehe selbst liegt auf Plan-Ebene (mdJE).
|
||||
// AHV-/PK-Renten: der Rechenweg der Rentenhöhe selbst liegt auf Plan-Ebene (mdJE).
|
||||
for (const e of orderedElements) {
|
||||
const ec = ecById.get(e.id)!;
|
||||
if (ec.trace || ec.startValue === 0) continue;
|
||||
@@ -1232,19 +1232,19 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
previousEndWealth = endWealthNominal;
|
||||
previousCashEnd = cashEnd;
|
||||
|
||||
// --- Uebergang: Carry aktualisieren, Cash der Folgephase bilden ---
|
||||
// --- Übergang: Carry aktualisieren, Cash der Folgephase bilden ---
|
||||
let txInflow = 0;
|
||||
let txImmediateRepay = 0;
|
||||
// Echte Vermoegensaenderungen an dieser Grenze (fuer die Bruecke der Folgephase).
|
||||
// Verkaeufe, Bezuege und Tilgungen sind fuer sich Umbuchungen -- vermoegenswirksam sind
|
||||
// nur die Steuer, die Verrentung (Kapital verlaesst die Bilanz) und die Differenz
|
||||
// Echte Vermögensänderungen an dieser Grenze (für die Brücke der Folgephase).
|
||||
// Verkäufe, Bezüge und Tilgungen sind für sich Umbuchungen -- vermögenswirksam sind
|
||||
// nur die Steuer, die Verrentung (Kapital verlässt die Bilanz) und die Differenz
|
||||
// zwischen Verkaufspreis und Verkehrswert.
|
||||
let txTax = 0;
|
||||
let txPensionConversion = 0;
|
||||
let txSaleGainLoss = 0;
|
||||
|
||||
// Einmalige Sonderein-/ausgaben auf dem Cash-Konto. Nur sinnvoll, wenn eine Folgephase
|
||||
// existiert -- nach der letzten Phase gibt es keinen Uebergang. Der Wechselkurs zwischen
|
||||
// existiert -- nach der letzten Phase gibt es keinen Übergang. Der Wechselkurs zwischen
|
||||
// real und nominal ist an dieser Grenze `cumulativeInflation` (Bestands-Deflator am
|
||||
// Phasenende), denn Cash ist ein Bestand.
|
||||
let txOneOffInflow = 0;
|
||||
@@ -1281,14 +1281,14 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
continue;
|
||||
}
|
||||
|
||||
// AHV: beim Pensions-Uebergang die geprueften Karriere-Werte vor Planbeginn uebernehmen.
|
||||
// AHV: beim Pensions-Übergang die geprüften Karriere-Werte vor Planbeginn übernehmen.
|
||||
if (e.category === "AHV") {
|
||||
if (ownerRetiresNext && owner) {
|
||||
const career = buildCareer(owner, ahvIncomeAccum, ahvYearsAccum, gapYearsByPerson);
|
||||
ahvBeforeByPerson.set(owner.id, {
|
||||
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schaetzung fuer die Jahre
|
||||
// vor Planbeginn -- exakt der Wert, den der Pruef-Dialog vorbelegt. Ein Fallback auf
|
||||
// 0 wuerde die Rente still und massiv zu tief rechnen. Beide Werte sind BRUTTO.
|
||||
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schätzung für die Jahre
|
||||
// vor Planbeginn -- exakt der Wert, den der Prüf-Dialog vorbelegt. Ein Fallback auf
|
||||
// 0 würde die Rente still und massiv zu tief rechnen. Beide Werte sind BRUTTO.
|
||||
avg: num(td.avgIncomeBefore, career.plannedAvgGrossIncome),
|
||||
gap: Math.max(0, Math.round(num(td.gapYearsBefore))),
|
||||
});
|
||||
@@ -1330,7 +1330,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
carry.value = 0;
|
||||
}
|
||||
} else {
|
||||
// Vorbezug (z. B. Wohneigentum/Selbststaendigkeit): ebenfalls kapitalbezugssteuerpflichtig.
|
||||
// Vorbezug (z. B. Wohneigentum/Selbstständigkeit): ebenfalls kapitalbezugssteuerpflichtig.
|
||||
// Das Kapital wird brutto entnommen, netto (nach Steuer) fliesst es ins Cash.
|
||||
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
|
||||
carry.value = ec.endValue - withdrawal;
|
||||
@@ -1360,7 +1360,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
txInflow += ec.endValue;
|
||||
carry.status = "SOLD";
|
||||
} else if (td.decision === "PARTIAL") {
|
||||
// Teilverkauf: Betrag fliesst ins Cash, der Rest bleibt aktiv und waechst weiter.
|
||||
// Teilverkauf: Betrag fliesst ins Cash, der Rest bleibt aktiv und wächst weiter.
|
||||
const part = Math.min(ec.endValue, Math.round(num(td.partialSaleAmount)));
|
||||
txInflow += part;
|
||||
carry.value = ec.endValue - part;
|
||||
@@ -1374,14 +1374,14 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (!re) break;
|
||||
if (td.decision === "SELL") {
|
||||
const salePrice = Math.round(num(td.salePrice));
|
||||
// Grundstueckgewinnsteuer bemisst sich am urspruenglichen Kaufpreis, nicht am
|
||||
// Grundstückgewinnsteuer bemisst sich am ursprünglichen Kaufpreis, nicht am
|
||||
// zwischenzeitlich gestiegenen Verkehrswert.
|
||||
const gain = Math.max(0, salePrice - re.purchase);
|
||||
const tax = gain * (num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE) / 100);
|
||||
txInflow += Math.round(salePrice - re.mortgage - tax);
|
||||
txTax += Math.round(tax);
|
||||
// Der Verkaufspreis kann vom fortgeschriebenen Verkehrswert abweichen -- diese
|
||||
// Differenz ist ein echter Vermoegensgewinn bzw. -verlust an der Grenze.
|
||||
// Differenz ist ein echter Vermögensgewinn bzw. -verlust an der Grenze.
|
||||
txSaleGainLoss += Math.round(salePrice - re.value);
|
||||
carry.status = "SOLD";
|
||||
} else {
|
||||
@@ -1409,7 +1409,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Rechenweg des Uebergangs je Element ---
|
||||
// --- Rechenweg des Übergangs je Element ---
|
||||
if (explain && nextPhase) {
|
||||
const inflowHere = txInflow - txStepsBefore;
|
||||
const taxHere = txTax - taxBefore;
|
||||
@@ -1492,8 +1492,8 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
incomingPensionConversion = txPensionConversion;
|
||||
incomingSaleGainLoss = txSaleGainLoss;
|
||||
|
||||
// Rechenweg des Cash-Uebergangs (einmalige Sonderein-/ausgaben). Wird nachtraeglich an
|
||||
// die Trace-Liste dieser Phase gehaengt -- `result` haelt die Referenz auf das Array.
|
||||
// Rechenweg des Cash-Übergangs (einmalige Sonderein-/ausgaben). Wird nachträglich an
|
||||
// die Trace-Liste dieser Phase gehängt -- `result` hält die Referenz auf das Array.
|
||||
if (explain && nextPhase && (txOneOffInflow !== 0 || txOneOffOutflow !== 0)) {
|
||||
const ct = phase.cashTransition ?? {};
|
||||
const steps: TraceStep[] = [];
|
||||
@@ -1541,7 +1541,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
specAnchor: "442-beitragskarriere-und-mdje",
|
||||
steps: [
|
||||
st("Beitragsjahre im Plan", career.planYears, "Summe (Phasendauer − Ausfalljahre)", undefined, undefined, "Jahre"),
|
||||
st("Durchschnittliches Bruttoeinkommen im Plan (real)", Math.round(career.plannedAvgGrossIncome), `Nettolohn × ${AHV_GROSS_FROM_NET_FACTOR}`, undefined,
|
||||
st("Durchschnittliches Bruttöinkommen im Plan (real)", Math.round(career.plannedAvgGrossIncome), `Nettolohn × ${AHV_GROSS_FROM_NET_FACTOR}`, undefined,
|
||||
"Das Tool erfasst netto, die AHV bemisst sich am Brutto. Der Faktor 1.12 ist ein Karriere-Durchschnitt und keine exakte Einzelfall-Umrechnung."),
|
||||
st("Beitragsjahre vor Planbeginn", Math.max(0, career.yearsBeforePlan - before.gap), "Alter bei Planbeginn − 21 − Ausfalljahre davor", undefined, undefined, "Jahre"),
|
||||
st("Durchschnittseinkommen vor Planbeginn (real, brutto)", Math.round(before.avg), undefined, undefined,
|
||||
@@ -1568,7 +1568,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
return { phases: result, yearly, nachlass, ruinAge, ahvCareer, traces: explain ? planTraces : undefined };
|
||||
}
|
||||
|
||||
// Durchschnittliches REALES Jahreseinkommen ueber eine Phase. Nominal waechst der Flow mit
|
||||
// Durchschnittliches REALES Jahreseinkommen über eine Phase. Nominal wächst der Flow mit
|
||||
// idx, real wird er mit der Inflation deflationiert -> geometrische Reihe mit q.
|
||||
function avgRealFlow(basis: number, idx: number, infl: number, duration: number, cumInflStart: number): number {
|
||||
const q = (1 + idx / 100) / (1 + infl / 100);
|
||||
@@ -1616,8 +1616,8 @@ export function planToCsv(plan: PlanInput, computed: PlanComputed): string {
|
||||
"Quote (Beginn)",
|
||||
"Quote (Ende)",
|
||||
"Cash (Ende)",
|
||||
"Endvermoegen (nominal)",
|
||||
"Endvermoegen (real)",
|
||||
"Endvermögen (nominal)",
|
||||
"Endvermögen (real)",
|
||||
];
|
||||
const rows = computed.phases.map((p) => [
|
||||
p.name,
|
||||
|
||||
Reference in New Issue
Block a user