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:
+45
-18
@@ -60,7 +60,10 @@ const CATEGORY_ICON: Record<ElementCategory, React.ReactNode> = {
|
||||
OTHER_DEBT: <CreditCard className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
// Kategorien mit einem Uebergangs-Entscheid. AHV ist dabei ein Sonderfall: nur beim
|
||||
// Pensions-Uebergang ist die Beitragskarriere zu pruefen (siehe transitionInactive).
|
||||
const TRANSITION_CATEGORIES: ElementCategory[] = [
|
||||
"AHV",
|
||||
"PENSION_FUND",
|
||||
"PILLAR_3A",
|
||||
"REAL_ESTATE",
|
||||
@@ -161,6 +164,18 @@ export function PlanView({
|
||||
return !!before?.working && !!after && !after.working;
|
||||
}
|
||||
|
||||
// Beitragskarriere des Element-Besitzers (nur fuer AHV relevant).
|
||||
function careerFor(element: ElementInput) {
|
||||
if (element.category !== "AHV" || !element.ownerRole || element.ownerRole === "HOUSEHOLD") return null;
|
||||
const person = plan.persons.find((p) => p.role === element.ownerRole);
|
||||
return computed.ahvCareer.find((c) => c.personId === person?.id) ?? null;
|
||||
}
|
||||
|
||||
// Anzahl Phasen NACH dieser (fuer den Fortschreibungs-Warnhinweis).
|
||||
function laterPhaseCount(phase: PhaseComputed): number {
|
||||
return computed.phases.length - phase.sequenceNumber;
|
||||
}
|
||||
|
||||
// Baut den Kontext fuer eine Phasenzelle.
|
||||
function buildPhaseContext(phase: PhaseComputed, element: ElementInput): CellContext {
|
||||
const ce = computedElement(phase.id, element.id);
|
||||
@@ -179,6 +194,8 @@ export function PlanView({
|
||||
carried: ce?.carried ?? false,
|
||||
derivedStart: ce?.baseValue ?? 0,
|
||||
deflatorStart: phase.cumulativeInflationStart,
|
||||
laterPhaseCount: laterPhaseCount(phase),
|
||||
ahvCareer: careerFor(element),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,14 +212,20 @@ export function PlanView({
|
||||
carried: ce?.carried ?? false,
|
||||
derivedStart: 0,
|
||||
deflatorStart: fromPhase.cumulativeInflationStart,
|
||||
laterPhaseCount: laterPhaseCount(fromPhase),
|
||||
ahvCareer: careerFor(element),
|
||||
};
|
||||
}
|
||||
|
||||
// Am Uebergang nichts (mehr) zu tun: verkauft/getilgt ODER PK/3a nach der Pensionierung
|
||||
// (Besitzer ist zu Beginn der Von-Phase bereits pensioniert -> bereits bezogen/verrentet).
|
||||
function transitionInactive(el: ElementInput, fromPhase: PhaseComputed): boolean {
|
||||
// (Besitzer ist zu Beginn der Von-Phase bereits pensioniert -> bereits bezogen/verrentet)
|
||||
// ODER AHV ausserhalb des Pensions-Uebergangs.
|
||||
function transitionInactive(el: ElementInput, fromPhase: PhaseComputed, toPhase?: PhaseComputed): boolean {
|
||||
const ce = computedElement(fromPhase.id, el.id);
|
||||
if (ce && ce.status !== "ACTIVE") return true;
|
||||
if (el.category === "AHV") {
|
||||
return !(toPhase && isRetirementTransition(el, fromPhase, toPhase));
|
||||
}
|
||||
if (el.category === "PENSION_FUND" || el.category === "PILLAR_3A") {
|
||||
if (el.ownerRole && el.ownerRole !== "HOUSEHOLD") {
|
||||
const owner = fromPhase.persons.find((p) => p.role === el.ownerRole);
|
||||
@@ -222,7 +245,7 @@ export function PlanView({
|
||||
let n = isCashTransitionAnswered(cashTransitionFor(fromPhase.id)) ? 0 : 1;
|
||||
for (const el of plan.elements) {
|
||||
if (!TRANSITION_CATEGORIES.includes(el.category)) continue;
|
||||
if (transitionInactive(el, fromPhase)) continue;
|
||||
if (transitionInactive(el, fromPhase, toPhase)) continue;
|
||||
const td = el.transitionValues[fromPhase.id] ?? {};
|
||||
const retire = toPhase ? isRetirementTransition(el, fromPhase, toPhase) : false;
|
||||
if (!isTransitionAnswered(el.category, retire, td)) n++;
|
||||
@@ -232,18 +255,15 @@ export function PlanView({
|
||||
|
||||
// Ist der Uebergangs-Entscheid dieses Elements noch offen?
|
||||
function transitionUnanswered(el: ElementInput, fromPhase: PhaseComputed, toPhase: PhaseComputed): boolean {
|
||||
if (transitionInactive(el, fromPhase)) return false;
|
||||
if (transitionInactive(el, fromPhase, toPhase)) return false;
|
||||
const retire = isRetirementTransition(el, fromPhase, toPhase);
|
||||
return !isTransitionAnswered(el.category, retire, el.transitionValues[fromPhase.id] ?? {});
|
||||
}
|
||||
|
||||
function transitionElements(fromPhase: PhaseComputed): ElementInput[] {
|
||||
function transitionElements(fromPhase: PhaseComputed, toPhase?: PhaseComputed): ElementInput[] {
|
||||
return plan.elements
|
||||
.filter((el) => TRANSITION_CATEGORIES.includes(el.category))
|
||||
.filter((el) => {
|
||||
const ce = computedElement(fromPhase.id, el.id);
|
||||
return !ce || ce.status === "ACTIVE";
|
||||
})
|
||||
.filter((el) => !transitionInactive(el, fromPhase, toPhase))
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex);
|
||||
}
|
||||
|
||||
@@ -461,7 +481,7 @@ export function PlanView({
|
||||
</td>
|
||||
);
|
||||
}
|
||||
const locked = TRANSITION_CATEGORIES.includes(el.category) && transitionInactive(el, col.fromPhase);
|
||||
const locked = TRANSITION_CATEGORIES.includes(el.category) && transitionInactive(el, col.fromPhase, col.toPhase);
|
||||
const canTransition = TRANSITION_CATEGORIES.includes(el.category) && !locked;
|
||||
const open = canTransition && transitionUnanswered(el, col.fromPhase, col.toPhase);
|
||||
return (
|
||||
@@ -571,7 +591,7 @@ export function PlanView({
|
||||
if (!fromPhase) return null;
|
||||
const toIndex = computed.phases.findIndex((p) => p.id === fromPhase.id) + 1;
|
||||
const toPhase = computed.phases[toIndex];
|
||||
const els = transitionElements(fromPhase);
|
||||
const els = transitionElements(fromPhase, toPhase);
|
||||
return (
|
||||
<TransitionReviewDialog
|
||||
fromPhase={fromPhase}
|
||||
@@ -660,6 +680,7 @@ export function PlanView({
|
||||
phase={phaseInput}
|
||||
maxDurationYears={phase.maxDurationYears}
|
||||
isLast={isLast}
|
||||
laterPhaseCount={laterPhaseCount(phase)}
|
||||
onSaved={onChanged}
|
||||
onDeleted={() => {
|
||||
setSelected(null);
|
||||
@@ -679,6 +700,8 @@ export function PlanView({
|
||||
function transitionSummary(el: ElementInput, fromPhase: PhaseComputed, toPhase: PhaseComputed): string {
|
||||
const td = el.transitionValues[fromPhase.id] ?? {};
|
||||
switch (el.category) {
|
||||
case "AHV":
|
||||
return td.reviewed === true ? "Geprueft" : "?";
|
||||
case "REAL_ESTATE":
|
||||
case "OTHER_ASSET":
|
||||
return td.decision === "SELL" ? "Verkauf" : td.decision === "HOLD" ? "Halten" : "?";
|
||||
@@ -913,6 +936,8 @@ function AddElementDialog({
|
||||
carried: false,
|
||||
derivedStart: 0,
|
||||
deflatorStart: firstPhase.cumulativeInflationStart,
|
||||
laterPhaseCount: 0, // beim Anlegen bewusst kein Warnhinweis
|
||||
ahvCareer: null,
|
||||
};
|
||||
|
||||
async function create() {
|
||||
@@ -1175,13 +1200,15 @@ function TransitionReviewDialog({
|
||||
const ctx = buildContext(el);
|
||||
const retire = isRetirement(el);
|
||||
const hint =
|
||||
(el.category === "PENSION_FUND" || el.category === "PILLAR_3A") && !retire
|
||||
? "Hier könnten Sie optional Kapital beziehen."
|
||||
: el.category === "PENSION_FUND" && retire
|
||||
? "Pensionierung: Bezugsart wählen (Rente / Kapital / Kombination)."
|
||||
: el.category === "PILLAR_3A" && retire
|
||||
? "Wird bei Pensionierung vollständig bezogen."
|
||||
: null;
|
||||
el.category === "AHV" && retire
|
||||
? "Pensionierung: Beitragskarriere prüfen – die Rente hängt vom Durchschnittseinkommen ab."
|
||||
: (el.category === "PENSION_FUND" || el.category === "PILLAR_3A") && !retire
|
||||
? "Hier könnten Sie optional Kapital beziehen."
|
||||
: el.category === "PENSION_FUND" && retire
|
||||
? "Pensionierung: Bezugsart wählen (Rente / Kapital / Kombination)."
|
||||
: el.category === "PILLAR_3A" && retire
|
||||
? "Wird bei Pensionierung vollständig bezogen."
|
||||
: null;
|
||||
return (
|
||||
<div key={el.id} className="rounded-xl border border-border bg-surface-2 p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user