Das Pensionsalter laesst sich neu veraendern: Nicht das Alter wird gesetzt, sondern die Phasengrenze verschoben -- Vorphase laenger, Folgephase kuerzer, Gesamtdauer gleich. Faellt eine Phase dabei weg, werden die beiden Uebergaenge nach Bestaetigung zusammengelegt. - neues reines Modul lib/retirement.ts + POST /api/scenarios/<id>/retirement - Pensionsalter als Tornado-Treiber und Live-Simulations-Regler - AHV-Referenzalter 65: Rente ab 65 unabhaengig vom Pensionsalter; vor 65 Beitrag als Nichterwerbstaetige(r) (neues Feld ahvContribution). AHV wird dafuer jahresweise statt phasenweise gerechnet. - Punkt B: personenzugeordnetes Einkommen faellt bei Pensionierung auf 0 - Punkt A: Wiederkehr-Parameter werden live aus der Vorphase geerbt, sichtbar als "Aus Vorphase uebernehmen" - Punkt C: Kapitalzufluss am Pensions-Uebergang per Quote auf Amortisation, Anlage und Cash verteilbar - Fix: carry.flowBasis wurde vor der Jahresschleife berechnet, effektive Werte kamen deshalb nie in der Folgephase an SPEZIFIKATION 0.26 (neue Kapitel 3.12, 4.4.7, 4.16). 221 -> 261 Tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
} from "@/components/ElementDetail";
|
||||
import { PhaseDetail } from "@/components/PhaseDetail";
|
||||
import { PlanProfileFields, type ProfileDraft } from "@/components/PlanProfileFields";
|
||||
import { RetirementAdjuster } from "@/components/RetirementAdjuster";
|
||||
import { MoneyField } from "@/components/FormField";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { formatChf } from "@/lib/format";
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
CATEGORY_LABELS,
|
||||
CATEGORY_ORDER,
|
||||
PERSON_ONLY_CATEGORIES,
|
||||
inheritedPhaseValues,
|
||||
num,
|
||||
type CashTransitionData,
|
||||
type ElementCategory,
|
||||
@@ -256,6 +258,16 @@ export function PlanView({
|
||||
return computed.phases.length - phase.sequenceNumber;
|
||||
}
|
||||
|
||||
// Phasen in ihrer Reihenfolge -- Grundlage der Vererbung (Roadmap Nr. 44, Punkt A).
|
||||
const orderedPhaseIds = computed.phases.map((p) => p.id);
|
||||
|
||||
// Alter des Element-Besitzers im ersten Jahr einer Phase. Ohne Personenzuordnung gilt
|
||||
// Person A, weil die Zeitachse ohnehin an ihr hängt.
|
||||
function ownerAgeAtPhaseStart(phase: PhaseComputed, element: ElementInput): number {
|
||||
const role = element.ownerRole && element.ownerRole !== "HOUSEHOLD" ? element.ownerRole : "PERSON_A";
|
||||
return phase.persons.find((p) => p.role === role)?.startAge ?? 0;
|
||||
}
|
||||
|
||||
// Baut den Kontext für eine Phasenzelle.
|
||||
function buildPhaseContext(phase: PhaseComputed, element: ElementInput): CellContext {
|
||||
const ce = computedElement(phase.id, element.id);
|
||||
@@ -280,6 +292,8 @@ export function PlanView({
|
||||
deflatorStart: phase.cumulativeInflationStart,
|
||||
laterPhaseCount: laterPhaseCount(phase),
|
||||
ahvCareer: careerFor(element),
|
||||
inheritedValues: inheritedPhaseValues(element.phaseValues, orderedPhaseIds, phase.id),
|
||||
ownerAgeStart: ownerAgeAtPhaseStart(phase, element),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,6 +316,8 @@ export function PlanView({
|
||||
deflatorStart: fromPhase.cumulativeInflationStart,
|
||||
laterPhaseCount: laterPhaseCount(fromPhase),
|
||||
ahvCareer: careerFor(element),
|
||||
inheritedValues: {},
|
||||
ownerAgeStart: ownerAgeAtPhaseStart(fromPhase, element),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1468,6 +1484,9 @@ function AddElementDialog({
|
||||
deflatorStart: firstPhase.cumulativeInflationStart,
|
||||
laterPhaseCount: 0, // beim Anlegen bewusst kein Warnhinweis
|
||||
ahvCareer: null,
|
||||
// Beim Anlegen gibt es keine Vorphase, aus der etwas zu erben waere.
|
||||
inheritedValues: {},
|
||||
ownerAgeStart: firstPhase.persons.find((p) => p.role === (owner ?? "PERSON_A"))?.startAge ?? 0,
|
||||
};
|
||||
|
||||
async function create() {
|
||||
@@ -1615,6 +1634,14 @@ function AddPhaseDialog({
|
||||
);
|
||||
}
|
||||
|
||||
// Ziele für die Anlage-Quote der Kapitalverwendung (Roadmap Nr. 44, Punkt C): alle
|
||||
// Vermögens-Elemente, die in der Folgephase noch aktiv sind.
|
||||
function investTargetsOf(toPhase: PhaseComputed | undefined): { id: string; name: string }[] {
|
||||
return (toPhase?.elements ?? [])
|
||||
.filter((e) => e.category === "OTHER_ASSET" && e.status === "ACTIVE")
|
||||
.map((e) => ({ id: e.elementId, name: e.name }));
|
||||
}
|
||||
|
||||
// --- Panel: Szenario-Profil (Grundprofil bearbeiten) ---
|
||||
function ProfilePanel({ plan, onClose, onSaved }: { plan: PlanInput; onClose: () => void; onSaved: () => void }) {
|
||||
const [draft, setDraft] = useState<ProfileDraft>({
|
||||
@@ -1642,12 +1669,21 @@ function ProfilePanel({ plan, onClose, onSaved }: { plan: PlanInput; onClose: ()
|
||||
return (
|
||||
<InspectorShell title="Szenario-Profil" subtitle="Haushalt, Personen, Planstart und Inflation dieses Szenarios" onClose={onClose}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<PlanProfileFields draft={draft} onChange={setDraft} />
|
||||
<PlanProfileFields draft={draft} onChange={setDraft} lockRetirement={plan.phases.length > 0} />
|
||||
{error && <p className="text-sm text-danger">{error}</p>}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button disabled={saving} onClick={save}>{saving ? "…" : "Speichern"}</Button>
|
||||
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
|
||||
</div>
|
||||
|
||||
{plan.phases.length > 0 && (
|
||||
<div className="mt-3 border-t border-border pt-3">
|
||||
<h3 className="mb-2 text-sm font-semibold text-fg">Pensionsalter anpassen</h3>
|
||||
{/* Wirkt SOFORT (eigener Endpunkt), weil dabei Phasendauern verschoben werden --
|
||||
das lässt sich nicht sinnvoll mit dem Profil-Formular zusammen abspeichern. */}
|
||||
<RetirementAdjuster plan={plan} onSaved={onSaved} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InspectorShell>
|
||||
);
|
||||
@@ -1723,6 +1759,9 @@ function TransitionReviewDialog({
|
||||
ct={ct}
|
||||
setC={(patch) => setCt((prev) => ({ ...prev, ...patch }))}
|
||||
deflatorEnd={fromPhase.cumulativeInflationEnd}
|
||||
showCapitalUse={(toPhase?.capitalInflow ?? 0) > 0}
|
||||
capitalInflow={toPhase?.capitalInflow ?? 0}
|
||||
investTargets={investTargetsOf(toPhase)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1814,6 +1853,9 @@ function CashTransitionPanel({
|
||||
ct={ct}
|
||||
setC={(patch) => setCt((prev) => ({ ...prev, ...patch }))}
|
||||
deflatorEnd={fromPhase.cumulativeInflationEnd}
|
||||
showCapitalUse={(toPhase?.capitalInflow ?? 0) > 0}
|
||||
capitalInflow={toPhase?.capitalInflow ?? 0}
|
||||
investTargets={investTargetsOf(toPhase)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="mt-3 text-sm text-danger">{error}</p>}
|
||||
|
||||
Reference in New Issue
Block a user