Modul-Review 4: Matrix -- Kapitalverwendung, Phasendauer, Bedienung
Deploy App / deploy (push) Successful in 1m10s
Deploy App / deploy (push) Successful in 1m10s
Kapitalverwendung (Punkt C) am richtigen Ort: - Prozent-Aufteilung des bezogenen Alterskapitals wandert vom Cash-Uebergang zum Bezugs-Entscheid der PK (nur bei Kapitalbezug) bzw. der Saeule 3a -- mit zwei Guthaben liess sie sich vorher gar nicht getrennt beantworten - Dialoge fuehren neu brutto -> Steuersatz -> netto -> Verteilung - BUGFIX: Der zugeteilte Betrag erhoehte still den internen Bestand, deshalb zeigten Ziel-Element und "Kapital verteilen" eine 0. Er laeuft jetzt ueber Carry.capitalIn als Zusatzeinlage der Folgephase und ist ueberall sichtbar - Saeule 3a ist am Pensions-Uebergang neu ein offener Entscheid Phasendauer (gemeldeter Fehler): - Die Folgephase gleicht eine geaenderte Dauer aus; Gesamtdauer bleibt gleich - Vorher kappte das Tool nur die bearbeitete Phase -> Phase 2 ueberspannte danach die Pensionierung und die Invariante aus Punkt 44 kippte - Rueckfrage vorher, Blockade wenn die Folgephase unter 1 Jahr fiele - neue reine Funktion planDurationChange Bedienung: - Element-Zeile und Phasenkopf: Stift (umbenennen, beim Element inkl. Zuordnung), Papierkorb, Expand -- alle immer sichtbar - PATCH /api/elements/<id> nimmt neu auch ownerRole - Hilfetexte via Portal (wurden in scrollenden Dialogen abgeschnitten) - Verteil-Dialoge: Zuordnung je Zeile, nach vom/ins Cash gruppiert, Vorbelegung mit dem EFFEKTIVEN Wert inkl. Vererbung (zeigte vorher 0) - Matrix: gleiche Spaltenbreiten + horizontales Scrollen, "Alle auf-/ zuklappen", Kategorie-Summe in der zugeklappten Zeile - Phasen-Detailansicht nutzt die neue Aufteilungs-Grafik - Uebersicht: "Leer starten" auch im leeren Zustand Nebenbei: dritte verstuemmelte Hex-Farbe (#7c3aed) repariert, Phasen-Panel nutzt den eigenen Bestaetigungs-Dialog statt window.confirm; mehrere veraltete Referenzen und die buildCarryData-Tabelle in der Spez nachgezogen. SPEZIFIKATION 0.33. 278 -> 288 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,11 @@ export interface CellContext {
|
||||
// Alter des Element-Besitzers im ersten Jahr dieser Phase. Entscheidet bei der AHV, ob
|
||||
// das Referenzalter in diese Phase fällt (Kap. 4.4.6).
|
||||
ownerAgeStart: number;
|
||||
// Mögliche Ziele der Anlage-Quote beim Kapitalbezug (Punkt C): die in der FOLGEphase noch
|
||||
// aktiven Vermögens-Elemente.
|
||||
investTargets: { id: string; name: string }[];
|
||||
// Betrag, der aus einem Kapitalbezug in DIESES Element umgeleitet wurde (Punkt C).
|
||||
capitalFromTransfer: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -253,18 +258,10 @@ export function CashTransitionFields({
|
||||
ct,
|
||||
setC,
|
||||
deflatorEnd,
|
||||
// Roadmap Nr. 44, Punkt C: Nur am Pensions-Übergang gefragt -- dort kommt das Kapital
|
||||
// (PK, 3a, Immobilienverkauf) auf einmal herein und will verwendet werden.
|
||||
showCapitalUse = false,
|
||||
capitalInflow = 0,
|
||||
investTargets = [],
|
||||
}: {
|
||||
ct: CashTransitionData;
|
||||
setC: (patch: Partial<CashTransitionData>) => void;
|
||||
deflatorEnd: number; // Bestands-Deflator an der Phasengrenze
|
||||
showCapitalUse?: boolean;
|
||||
capitalInflow?: number;
|
||||
investTargets?: { id: string; name: string }[];
|
||||
}) {
|
||||
const mode = ct.mode ?? "NONE";
|
||||
const showIn = mode === "INFLOW" || mode === "BOTH";
|
||||
@@ -365,39 +362,41 @@ export function CashTransitionFields({
|
||||
</>
|
||||
)}
|
||||
|
||||
{showCapitalUse && <CapitalUseFields ct={ct} setC={setC} inflow={capitalInflow} targets={investTargets} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Verwendung des Kapitalzuflusses am Pensions-Übergang (Roadmap Nr. 44, Punkt C).
|
||||
// Verwendung des BEZOGENEN ALTERSKAPITALS (Roadmap Nr. 44, Punkt C).
|
||||
//
|
||||
// Steht am Bezugs-Entscheid des jeweiligen Guthabens (PK bzw. Säule 3a) -- bis 0.32 hing die
|
||||
// Frage am Cash-Übergang, wo sie sich mit zwei Guthaben gar nicht getrennt beantworten liess.
|
||||
//
|
||||
// In PROZENT und nicht in Franken: Verschiebt man das Pensionsalter, ändert sich das bezogene
|
||||
// Kapital. Ein Frankenbetrag müsste dann von Hand nachgezogen werden -- und würde bis dahin
|
||||
// still eine falsche Aufteilung rechnen. Eine Quote skaliert mit.
|
||||
function CapitalUseFields({
|
||||
ct,
|
||||
setC,
|
||||
inflow,
|
||||
export function CapitalUseFields({
|
||||
td,
|
||||
setT,
|
||||
netAmount,
|
||||
targets,
|
||||
}: {
|
||||
ct: CashTransitionData;
|
||||
setC: (patch: Partial<CashTransitionData>) => void;
|
||||
inflow: number;
|
||||
td: TransitionData;
|
||||
setT: (patch: Partial<TransitionData>) => void;
|
||||
// Netto-Auszahlung, auf die sich die Prozente beziehen.
|
||||
netAmount: number;
|
||||
targets: { id: string; name: string }[];
|
||||
}) {
|
||||
const amort = Math.max(0, Math.min(100, num(ct.capitalUseAmortizationPct)));
|
||||
const invest = Math.max(0, Math.min(100 - amort, num(ct.capitalUseInvestPct)));
|
||||
const amort = Math.max(0, Math.min(100, num(td.capitalUseAmortizationPct)));
|
||||
const invest = Math.max(0, Math.min(100 - amort, num(td.capitalUseInvestPct)));
|
||||
const cash = Math.max(0, 100 - amort - invest);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sm:col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
<strong className="text-fg">Verwendung des Kapitalzuflusses</strong> (PK-Kapital, Säule 3a,
|
||||
Verkaufserlös). Bei diesem Übergang fliessen{" "}
|
||||
<strong className="text-fg">{formatChf(inflow)}</strong> netto herein. Erfasst wird die
|
||||
Aufteilung in Prozent – so bleibt sie richtig, wenn du das Pensionsalter verschiebst und
|
||||
sich der Betrag dadurch ändert. Was nicht zugeteilt ist, bleibt Cash.
|
||||
<div className="sm:col-span-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
<strong className="text-fg">Wohin fliesst das bezogene Kapital?</strong> Von den{" "}
|
||||
<strong className="text-fg">{formatChf(netAmount)}</strong> netto teilst du hier in Prozent zu –
|
||||
so bleibt die Aufteilung richtig, wenn sich der Betrag durch ein anderes Pensionsalter ändert.
|
||||
Was nicht zugeteilt ist, bleibt auf dem Cash-Konto.
|
||||
</div>
|
||||
<NumberField
|
||||
label="… in die Hypothek (Amortisation, %)"
|
||||
@@ -406,31 +405,38 @@ function CapitalUseFields({
|
||||
min={0}
|
||||
max={100}
|
||||
value={amort}
|
||||
onChange={(v) => setC({ capitalUseAmortizationPct: Math.max(0, Math.min(100, v)) })}
|
||||
onChange={(v) => setT({ capitalUseAmortizationPct: Math.max(0, Math.min(100, v)) })}
|
||||
/>
|
||||
<NumberField
|
||||
label="… in die Anlage (%)"
|
||||
help="Fliesst als Zusatzinvestition in das gewählte Vermögens-Element und wächst dort mit dessen Rendite weiter."
|
||||
help="Fliesst als Zusatzeinlage in das gewählte Vermögens-Element der Folgephase und wächst dort mit dessen Rendite weiter."
|
||||
step={5}
|
||||
min={0}
|
||||
max={100 - amort}
|
||||
value={invest}
|
||||
onChange={(v) => setC({ capitalUseInvestPct: Math.max(0, Math.min(100 - amort, v)) })}
|
||||
onChange={(v) => setT({ capitalUseInvestPct: Math.max(0, Math.min(100 - amort, v)) })}
|
||||
/>
|
||||
{invest > 0 && targets.length > 0 && (
|
||||
<div className="sm:col-span-2">
|
||||
<SelectField
|
||||
label="Ziel der Anlage-Quote"
|
||||
help="Welches Vermögens-Element wird aufgestockt?"
|
||||
value={ct.capitalUseTargetElementId ?? targets[0].id}
|
||||
onChange={(v: string) => setC({ capitalUseTargetElementId: v })}
|
||||
value={td.capitalUseTargetElementId ?? targets[0].id}
|
||||
onChange={(v: string) => setT({ capitalUseTargetElementId: v })}
|
||||
options={targets.map((t) => ({ value: t.id, label: t.name }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invest > 0 && (
|
||||
<DerivedField
|
||||
label="… in die Anlage (Betrag)"
|
||||
value={Math.round((netAmount * invest) / 100)}
|
||||
help="Erscheint in der Folgephase als Zusatzeinlage beim gewählten Element."
|
||||
/>
|
||||
)}
|
||||
<DerivedField
|
||||
label={`… bleibt Cash (${cash} %)`}
|
||||
value={Math.round((inflow * cash) / 100)}
|
||||
value={Math.round((netAmount * cash) / 100)}
|
||||
help="Der nicht zugeteilte Rest. Wird automatisch berechnet."
|
||||
/>
|
||||
</>
|
||||
@@ -795,9 +801,17 @@ export function ElementPhaseFields({
|
||||
label="Zusatzinvestition aus Kapital (CHF)"
|
||||
help="Neuinvestition aus dem verfügbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
/>
|
||||
{/* Aus einem Kapitalbezug (PK/3a) am letzten Übergang umgeleitet -- nicht hier
|
||||
erfasst, sondern dort als Quote entschieden (Punkt C). */}
|
||||
{context.capitalFromTransfer > 0 && (
|
||||
<DerivedField
|
||||
label="Davon aus Kapitalbezug (PK/3a)"
|
||||
value={context.capitalFromTransfer}
|
||||
help="Beim letzten Übergang wurde ein Teil des bezogenen Alterskapitals hierher umgeleitet. Die Quote änderst du im Bezugs-Entscheid der Pensionskasse bzw. der Säule 3a."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MoneyField label="Startwert (CHF)" value={num(pd.startValue)} onChange={(v) => setP({ startValue: v })} />
|
||||
@@ -873,9 +887,16 @@ export function ElementTransitionFields({
|
||||
Die Beitragskarriere wird erst beim Übergang in die Pensionierung geprüft.
|
||||
</p>
|
||||
);
|
||||
case "PENSION_FUND":
|
||||
case "PENSION_FUND": {
|
||||
if (context.isRetirementTransition) {
|
||||
const mode = td.payoutMode ?? "PENSION";
|
||||
const taxRate = num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE);
|
||||
// Brutto = ganzes Guthaben (Kapitalbezug) bzw. der gewählte Teil (Kombination).
|
||||
const brutto =
|
||||
mode === "COMBI"
|
||||
? Math.min(context.carriedEndValue, Math.round(num(td.capitalAmount)))
|
||||
: context.carriedEndValue;
|
||||
const netto = Math.round(brutto * (1 - taxRate / 100));
|
||||
return (
|
||||
<>
|
||||
<SelectField
|
||||
@@ -897,9 +918,6 @@ export function ElementTransitionFields({
|
||||
onChange={(v) => setT({ conversionRate: v })}
|
||||
/>
|
||||
)}
|
||||
{(mode === "CAPITAL" || mode === "COMBI") && (
|
||||
<NumberField label="Kapitalbezugssteuer (%)" step={0.5} value={num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)} onChange={(v) => setT({ capitalTaxRate: v })} />
|
||||
)}
|
||||
{mode === "COMBI" && (
|
||||
<MoneyField
|
||||
label="Davon Kapitalbezug (CHF)"
|
||||
@@ -909,23 +927,67 @@ export function ElementTransitionFields({
|
||||
onChange={(v) => setT({ capitalAmount: v })}
|
||||
/>
|
||||
)}
|
||||
{/* Erst der Betrag, dann die Steuer, dann die Verwendung -- in der Reihenfolge,
|
||||
in der man die Entscheidung tatsächlich trifft. */}
|
||||
{(mode === "CAPITAL" || mode === "COMBI") && (
|
||||
<>
|
||||
<DerivedField
|
||||
label="Kapitalbezug brutto"
|
||||
value={brutto}
|
||||
help="Das Guthaben am Ende der Vorphase (bei einer Kombination der oben gewählte Anteil)."
|
||||
/>
|
||||
<NumberField
|
||||
label="Kapitalbezugssteuer (%)"
|
||||
help="Pauschalsatz – die tatsächliche Steuer ist kantonal und progressiv."
|
||||
step={0.5}
|
||||
value={taxRate}
|
||||
onChange={(v) => setT({ capitalTaxRate: v })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Auszahlung netto"
|
||||
value={netto}
|
||||
help="Brutto abzüglich Kapitalbezugssteuer. Dieser Betrag wird unten verteilt."
|
||||
/>
|
||||
<CapitalUseFields td={td} setT={setT} netAmount={netto} targets={context.investTargets} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <WithdrawalDecision td={td} setT={setT} max={context.carriedEndValue} label="PK-Bezug (CHF)" />;
|
||||
case "PILLAR_3A":
|
||||
}
|
||||
case "PILLAR_3A": {
|
||||
if (context.isRetirementTransition) {
|
||||
// Die Säule 3a wird bei der Pensionierung IMMER vollständig bezogen -- entschieden
|
||||
// wird hier der Steuersatz und die Verwendung des Geldes.
|
||||
const taxRate = num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE);
|
||||
const brutto = context.carriedEndValue;
|
||||
const netto = Math.round(brutto * (1 - taxRate / 100));
|
||||
return (
|
||||
<NumberField
|
||||
label="Kapitalbezugssteuer (%)"
|
||||
help="Die Säule 3a wird bei Pensionierung vollständig bezogen."
|
||||
step={0.5}
|
||||
value={num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)}
|
||||
onChange={(v) => setT({ capitalTaxRate: v })}
|
||||
/>
|
||||
<>
|
||||
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
Die Säule 3a wird bei der Pensionierung <strong className="text-fg">vollständig bezogen</strong>. Zu
|
||||
entscheiden sind der Steuersatz und die Verwendung des Geldes.
|
||||
</p>
|
||||
<DerivedField label="Bezug brutto" value={brutto} help="Das Guthaben am Ende der Vorphase." />
|
||||
<NumberField
|
||||
label="Kapitalbezugssteuer (%)"
|
||||
help="Pauschalsatz – die tatsächliche Steuer ist kantonal und progressiv. Gestaffelte Bezüge (PK und 3a in verschiedenen Jahren) senken sie; das bildet das Tool noch nicht ab."
|
||||
step={0.5}
|
||||
value={taxRate}
|
||||
onChange={(v) => setT({ capitalTaxRate: v })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Auszahlung netto"
|
||||
value={netto}
|
||||
help="Brutto abzüglich Kapitalbezugssteuer. Dieser Betrag wird unten verteilt."
|
||||
/>
|
||||
<CapitalUseFields td={td} setT={setT} netAmount={netto} targets={context.investTargets} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <WithdrawalDecision td={td} setT={setT} max={context.carriedEndValue} label="3a-Bezug (CHF)" />;
|
||||
}
|
||||
case "REAL_ESTATE": {
|
||||
const decision = td.decision ?? "HOLD";
|
||||
const marktwert = Math.round(context.propertyValueEnd);
|
||||
|
||||
Reference in New Issue
Block a user