AHV einkommensabhaengig + Warnhinweis bei Aenderungen in frueheren Phasen
Deploy App / deploy (push) Successful in 59s
Deploy App / deploy (push) Successful in 59s
Roadmap Nr. 3: Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) ueber das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente. - Formel aus den amtlichen Randbedingungen hergeleitet und gegen die Tabelle 318.117.1 verifiziert: 51/51 Zeilen exakt. Schwellen sind Vielfache von R0=1'260 (12/36/72 x R0 = 15'120 / 45'360 / 90'720). Stuetzstellen als Golden Tests. - Alles REAL gerechnet: die AHV wertet vergangene Einkommen auf UND indexiert die Schwellen -- real hebt sich das auf. Nominal wuerde die Rente systematisch zu hoch ausfallen (Beispiel: faelschlich Maximalrente, ~41'600 ueber 25 Rentenjahre). - Pruefung der Beitragskarriere am Pensions-Uebergang; Zusatzfelder fuer die Jahre vor Planbeginn nur, wenn der Plan nicht bis Alter 21 zurueckreicht. - Sonderfall "bei Planbeginn bereits pensioniert": Felder in der Phasenzelle. - Ohne Pruefung gilt der geplante Durchschnitt (nicht 0) -- sonst waere die Rente still viel zu tief. - 13. Altersrente: Jahresbetrag = Monatsrente x 13. Roadmap Nr. 4: Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen existieren -- Werte schreiben sich fort und wirken bis ans Planende durch. Verhaltensaenderung fuer bestehende Plaene: siehe SPEZIFIKATION 9.10. Zwoelf Regressionstests (18 -> 30). Spezifikation auf v0.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { FieldLabel, MoneyField, NumberField, SelectField, TextField } from "@/components/FormField";
|
||||
import { formatChf } from "@/lib/format";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { ahvAnnualPension, ahvMdje, type AhvCareer } from "@/lib/calculations";
|
||||
import { CATEGORY_LABELS, num } from "@/lib/elements";
|
||||
import {
|
||||
DEFAULT_CAPITAL_TAX_RATE,
|
||||
@@ -31,6 +33,10 @@ export interface CellContext {
|
||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
||||
// Warnhinweis: Anzahl Phasen NACH dieser (Aenderungen schreiben sich dorthin fort).
|
||||
laterPhaseCount: number;
|
||||
// AHV-Beitragskarriere des Element-Besitzers (fuer die Pruefung am Pensions-Uebergang).
|
||||
ahvCareer: AhvCareer | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -42,6 +48,24 @@ interface Props {
|
||||
onDeleteElement: () => void;
|
||||
}
|
||||
|
||||
// Warnhinweis beim Bearbeiten einer Phase, der noch Phasen folgen. Seit V3 werden Werte live
|
||||
// fortgeschrieben (Endwert = Startwert der Folgephase) -- eine Aenderung hier wirkt also bis
|
||||
// ans Planende durch, was ohne Hinweis leicht uebersehen wird.
|
||||
export function CarryWarning({ laterPhaseCount }: { laterPhaseCount: number }) {
|
||||
if (laterPhaseCount <= 0) return null;
|
||||
const phasen = laterPhaseCount === 1 ? "die nachfolgende Lebensphase" : `die ${laterPhaseCount} nachfolgenden Lebensphasen`;
|
||||
return (
|
||||
<div className="col-span-full flex items-start gap-2 rounded-lg border border-danger bg-danger-soft px-3 py-2 text-xs text-danger">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>
|
||||
<strong>Achtung: Diese Lebensphase ist nicht die letzte.</strong> Werte werden fortgeschrieben
|
||||
(Endwert = Startwert der Folgephase). Eine Aenderung hier wirkt sich auf {phasen} aus und
|
||||
veraendert deren Startwerte, Kennzahlen sowie Endvermoegen, Cash-Verlauf und Ruinalter.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Read-only Anzeige eines abgeleiteten (fortgeschriebenen) Wertes.
|
||||
function DerivedField({ label, value, help }: { label: string; value: number; help?: string }) {
|
||||
return (
|
||||
@@ -111,9 +135,86 @@ function WithdrawalDecision({
|
||||
|
||||
// Vorbelegung expliziter Entscheide, damit ein blosses "Speichern" den sichtbaren Default
|
||||
// (Halten / Kein Bezug / Rente) auch tatsaechlich persistiert.
|
||||
// AHV-Pruefung am Pensions-Uebergang: geplantes Durchschnittseinkommen aus dem Plan plus --
|
||||
// nur falls der Plan nicht bis zum Beitragsbeginn (Alter 21) zurueckreicht -- die Jahre davor.
|
||||
export function AhvReviewFields({
|
||||
career,
|
||||
td,
|
||||
setT,
|
||||
}: {
|
||||
career: AhvCareer;
|
||||
td: TransitionData;
|
||||
setT: (patch: Partial<TransitionData>) => void;
|
||||
}) {
|
||||
const brauchtVorgeschichte = career.yearsBeforePlan > 0;
|
||||
// Vorbelegung mit dem geplanten Durchschnitt: besser als 0, wenn der Nutzer nichts weiss.
|
||||
const avgBefore = typeof td.avgIncomeBefore === "number" ? td.avgIncomeBefore : Math.round(career.plannedAvgIncome);
|
||||
const gapBefore = Math.max(0, Math.round(num(td.gapYearsBefore)));
|
||||
const mdJE = ahvMdje(career, brauchtVorgeschichte ? avgBefore : 0, gapBefore);
|
||||
const rente = ahvAnnualPension(mdJE, career.gapYearsInPlan + gapBefore);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="col-span-full rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
Die AHV-Rente haengt vom <strong className="text-fg">massgebenden durchschnittlichen Jahreseinkommen</strong>{" "}
|
||||
ueber die ganze Beitragsdauer (ab Alter 21) ab. Alle Betraege sind REAL (heutige Kaufkraft) –
|
||||
die AHV wertet vergangene Einkommen auf und indexiert die Schwellen, was sich real weitgehend aufhebt.
|
||||
</p>
|
||||
|
||||
<DerivedField
|
||||
label="Geplantes Durchschnittseinkommen (aus dem Plan)"
|
||||
value={Math.round(career.plannedAvgIncome)}
|
||||
help={`Reales Mittel ueber ${career.planYears} Beitragsjahre der Erwerbsphasen dieses Plans. Nur Einkommen, die dieser Person zugeordnet sind.`}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Beitragsjahre im Plan"
|
||||
value={career.planYears}
|
||||
help="Dauer der Erwerbsphasen abzueglich der dort erfassten Ausfalljahre."
|
||||
/>
|
||||
|
||||
{brauchtVorgeschichte && (
|
||||
<>
|
||||
<p className="col-span-full rounded-lg border border-dashed border-border px-3 py-2 text-xs text-muted">
|
||||
Der Plan beginnt erst im Alter {career.yearsBeforePlan + 21}. Die {career.yearsBeforePlan} Beitragsjahre
|
||||
davor kennt das Tool nicht – bitte ergaenzen.
|
||||
</p>
|
||||
<MoneyField
|
||||
label="Durchschnittseinkommen vor Planbeginn (real)"
|
||||
help="In HEUTIGER Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung uebernehmen – dort ist die Aufwertung bereits enthalten. Die Rohbetraege im Kontoauszug sind historisch-nominal und daher zu tief."
|
||||
value={avgBefore}
|
||||
onChange={(v) => setT({ avgIncomeBefore: v })}
|
||||
/>
|
||||
<NumberField
|
||||
label="Ausfalljahre vor Planbeginn"
|
||||
help="Jahre ohne AHV-Beitraege zwischen Alter 21 und Planbeginn (z. B. Ausland, Studium). Jedes kuerzt die Rente um 1/44."
|
||||
value={gapBefore}
|
||||
min={0}
|
||||
max={career.yearsBeforePlan}
|
||||
onChange={(v) => setT({ gapYearsBefore: Math.max(0, Math.min(career.yearsBeforePlan, Math.round(v))) })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DerivedField
|
||||
label="Massgebendes durchschnittliches Jahreseinkommen"
|
||||
value={Math.round(mdJE)}
|
||||
help="Gewichtetes Mittel ueber alle Beitragsjahre (vor Planbeginn und im Plan). Ab 90'720 gibt es die Maximalrente."
|
||||
/>
|
||||
<DerivedField
|
||||
label="Resultierende AHV-Rente pro Jahr"
|
||||
value={rente}
|
||||
help="Vollrente zum mdJE (Skala 44), inkl. 13. Altersrente, gekuerzt um die Ausfalljahre. Bei Ehepaaren zusaetzlich plafoniert."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function withTransitionDefaults(category: ElementCategory, isRetirement: boolean, td: TransitionData): TransitionData {
|
||||
const out = { ...td };
|
||||
if (category === "REAL_ESTATE" || category === "OTHER_ASSET") {
|
||||
if (category === "AHV") {
|
||||
// Ein Speichern der Pruefung markiert sie als erledigt (gleiches Muster wie bei PK/3a).
|
||||
if (isRetirement && out.reviewed === undefined) out.reviewed = true;
|
||||
} else if (category === "REAL_ESTATE" || category === "OTHER_ASSET") {
|
||||
if (out.decision === undefined) out.decision = "HOLD";
|
||||
} else if (category === "PENSION_FUND") {
|
||||
if (isRetirement) {
|
||||
@@ -271,6 +372,9 @@ export function CashTransitionFields({
|
||||
// "Beantwortet" = ein konkreter Entscheid liegt vor (kein offenes Fragezeichen).
|
||||
export function isTransitionAnswered(category: ElementCategory, isRetirement: boolean, td: TransitionData): boolean {
|
||||
switch (category) {
|
||||
case "AHV":
|
||||
// Nur bei der Pensionierung ist eine Pruefung der Beitragskarriere noetig.
|
||||
return isRetirement ? td.reviewed === true : true;
|
||||
case "REAL_ESTATE":
|
||||
case "OTHER_ASSET":
|
||||
return td.decision !== undefined;
|
||||
@@ -342,10 +446,42 @@ export function ElementPhaseFields({
|
||||
}
|
||||
case "AHV":
|
||||
if (!context.ownerWorking) {
|
||||
// Sonderfall: bei Planbeginn bereits pensioniert -> es gibt keinen Pensions-Uebergang,
|
||||
// an dem die Karriere geprueft werden koennte. Dann hier erfassen (nur erste Phase).
|
||||
if (!carried && context.ahvCareer && context.ahvCareer.planYears === 0) {
|
||||
return (
|
||||
<>
|
||||
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
Diese Person ist bei Planbeginn bereits pensioniert. Die AHV-Rente haengt vom
|
||||
massgebenden durchschnittlichen Jahreseinkommen ueber die ganze Beitragsdauer ab –
|
||||
bitte hier erfassen (REAL, heutige Kaufkraft).
|
||||
</p>
|
||||
<MoneyField
|
||||
label="Durchschnittseinkommen der Beitragsjahre (real)"
|
||||
help="Am besten aus der AHV-Rentenvorausberechnung bzw. der Rentenverfuegung. Ab 90'720 gibt es die Maximalrente."
|
||||
value={num(pd.avgIncomeBefore)}
|
||||
onChange={(v) => setP({ avgIncomeBefore: v })}
|
||||
/>
|
||||
<NumberField
|
||||
label="Ausfalljahre total"
|
||||
help="Jahre ohne AHV-Beitraege. Jedes kuerzt die Rente um 1/44."
|
||||
value={num(pd.gapYearsBefore)}
|
||||
min={0}
|
||||
onChange={(v) => setP({ gapYearsBefore: Math.max(0, Math.round(v)) })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Resultierende AHV-Rente pro Jahr"
|
||||
value={ahvAnnualPension(num(pd.avgIncomeBefore), num(pd.gapYearsBefore))}
|
||||
help="Inkl. 13. Altersrente. Bei Ehepaaren zusaetzlich plafoniert."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Die AHV-Rente wird automatisch aus den bisherigen Ausfalljahren berechnet (siehe Kennzahl in der
|
||||
Matrix). Bei Ehepaaren greift die Plafonierung auf 150% der Maximalrente.
|
||||
Die AHV-Rente wird aus der beim Pensions-Uebergang geprueften Beitragskarriere berechnet
|
||||
(massgebendes Durchschnittseinkommen und Ausfalljahre). Bei Ehepaaren greift die
|
||||
Plafonierung auf 150% der Maximalrente.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -517,13 +653,21 @@ export function ElementTransitionFields({
|
||||
switch (element.category) {
|
||||
case "INCOME":
|
||||
case "EXPENSE":
|
||||
case "AHV":
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Fuer diese Kategorie gibt es im Uebergang keine Eingaben. Die Werte werden 1:1 in die naechste
|
||||
Lebensphase uebernommen und koennen dort angepasst werden.
|
||||
</p>
|
||||
);
|
||||
case "AHV":
|
||||
if (context.isRetirementTransition && context.ahvCareer) {
|
||||
return <AhvReviewFields career={context.ahvCareer} td={td} setT={setT} />;
|
||||
}
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Die Beitragskarriere wird erst beim Uebergang in die Pensionierung geprueft.
|
||||
</p>
|
||||
);
|
||||
case "PENSION_FUND":
|
||||
if (context.isRetirementTransition) {
|
||||
const mode = td.payoutMode ?? "PENSION";
|
||||
@@ -689,6 +833,8 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{/* Punkt 4: Warnung, dass Aenderungen sich in die Folgephasen fortschreiben. */}
|
||||
{!isTransition && <CarryWarning laterPhaseCount={context.laterPhaseCount} />}
|
||||
{isTransition ? (
|
||||
<ElementTransitionFields element={element} context={context} td={td} setT={setT} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user