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:
@@ -29,18 +29,18 @@ export interface CellContext {
|
||||
isConsumption: boolean;
|
||||
durationYears: number;
|
||||
isRetirementTransition: boolean;
|
||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, für Bezugs-Maxima
|
||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||
derivedMortgage: number; // nur Immobilie: fortgeschriebene Resthypothek zu Phasenbeginn
|
||||
mortgageEnd: number; // nur Immobilie: Resthypothek am Phasenende (fuer die Sonderamortisation)
|
||||
mortgageEnd: number; // nur Immobilie: Resthypothek am Phasenende (für die Sonderamortisation)
|
||||
// nur Immobilie: modellierter VERKEHRSWERT am Phasenende (Eigenkapital + Resthypothek),
|
||||
// inkl. aufgelaufener Wertsteigerung. Vorbelegung und Vergleichswert fuer den Verkaufspreis.
|
||||
// inkl. aufgelaufener Wertsteigerung. Vorbelegung und Vergleichswert für den Verkaufspreis.
|
||||
propertyValueEnd: number;
|
||||
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
||||
// Warnhinweis: Anzahl Phasen NACH dieser (Aenderungen schreiben sich dorthin fort).
|
||||
// Warnhinweis: Anzahl Phasen NACH dieser (Änderungen schreiben sich dorthin fort).
|
||||
laterPhaseCount: number;
|
||||
// AHV-Beitragskarriere des Element-Besitzers (fuer die Pruefung am Pensions-Uebergang).
|
||||
// AHV-Beitragskarriere des Element-Besitzers (für die Prüfung am Pensions-Übergang).
|
||||
ahvCareer: AhvCareer | null;
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ interface Props {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// fortgeschrieben (Endwert = Startwert der Folgephase) -- eine Änderung hier wirkt also bis
|
||||
// ans Planende durch, was ohne Hinweis leicht übersehen wird.
|
||||
export function CarryWarning({ laterPhaseCount }: { laterPhaseCount: number }) {
|
||||
if (laterPhaseCount <= 0) return null;
|
||||
const phasen = laterPhaseCount === 1 ? "die nachfolgende Lebensphase" : `die ${laterPhaseCount} nachfolgenden Lebensphasen`;
|
||||
@@ -64,8 +64,8 @@ export function CarryWarning({ laterPhaseCount }: { laterPhaseCount: number }) {
|
||||
<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.
|
||||
(Endwert = Startwert der Folgephase). Eine Änderung hier wirkt sich auf {phasen} aus und
|
||||
verändert deren Startwerte, Kennzahlen sowie Endvermögen, Cash-Verlauf und Ruinalter.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -83,7 +83,7 @@ function DerivedField({ label, value, help }: { label: string; value: number; he
|
||||
);
|
||||
}
|
||||
|
||||
// PK/3a-Bezugs-Entscheid im normalen Uebergang (Vorbezug): Kein Bezug / Bezug (+ Betrag +
|
||||
// PK/3a-Bezugs-Entscheid im normalen Übergang (Vorbezug): Kein Bezug / Bezug (+ Betrag +
|
||||
// Kapitalbezugssteuer). Der Bezugsbetrag wird brutto dem Kapital entnommen; ins Cash fliesst
|
||||
// der Betrag nach Abzug der Steuer.
|
||||
function WithdrawalDecision({
|
||||
@@ -130,7 +130,7 @@ function WithdrawalDecision({
|
||||
<DerivedField
|
||||
label="Auszahlung netto (ins Cash)"
|
||||
value={Math.round(gross * (1 - taxRate / 100))}
|
||||
help="Bruttobezug abzueglich Kapitalbezugssteuer. Wird automatisch berechnet."
|
||||
help="Bruttobezug abzüglich Kapitalbezugssteuer. Wird automatisch berechnet."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -139,9 +139,9 @@ 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.
|
||||
// (Halten / Kein Bezug / Rente) auch tatsächlich persistiert.
|
||||
// AHV-Prüfung am Pensions-Übergang: geplantes Durchschnittseinkommen aus dem Plan plus --
|
||||
// nur falls der Plan nicht bis zum Beitragsbeginn (Alter 21) zurückreicht -- die Jahre davor.
|
||||
export function AhvReviewFields({
|
||||
career,
|
||||
td,
|
||||
@@ -162,39 +162,39 @@ export function AhvReviewFields({
|
||||
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. Massgebend ist der <strong className="text-fg">Bruttolohn</strong>;
|
||||
das Tool rechnet die im Plan erfassten Nettoeinkommen dafuer automatisch hoch. Alle Betraege sind
|
||||
Die AHV-Rente hängt vom <strong className="text-fg">massgebenden durchschnittlichen Jahreseinkommen</strong>{" "}
|
||||
über die ganze Beitragsdauer (ab Alter 21) ab. Massgebend ist der <strong className="text-fg">Bruttolohn</strong>;
|
||||
das Tool rechnet die im Plan erfassten Nettöinkommen dafür automatisch hoch. Alle Beträge sind
|
||||
REAL (heutige Kaufkraft) – die AHV wertet vergangene Einkommen auf und indexiert die Schwellen,
|
||||
was sich real weitgehend aufhebt.
|
||||
</p>
|
||||
|
||||
<DerivedField
|
||||
label="Geplantes Bruttoeinkommen (aus dem Plan)"
|
||||
label="Geplantes Bruttöinkommen (aus dem Plan)"
|
||||
value={Math.round(career.plannedAvgGrossIncome)}
|
||||
help={`Reales Mittel ueber ${career.planYears} Beitragsjahre der Erwerbsphasen dieses Plans, hochgerechnet vom Netto- auf den Bruttolohn. Nur Einkommen, die dieser Person zugeordnet sind.`}
|
||||
help={`Reales Mittel über ${career.planYears} Beitragsjahre der Erwerbsphasen dieses Plans, hochgerechnet vom Netto- auf den Bruttolohn. Nur Einkommen, die dieser Person zugeordnet sind.`}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Beitragsjahre im Plan"
|
||||
value={career.planYears}
|
||||
help="Dauer der Erwerbsphasen abzueglich der dort erfassten Ausfalljahre."
|
||||
help="Dauer der Erwerbsphasen abzüglich 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.
|
||||
davor kennt das Tool nicht – bitte ergänzen.
|
||||
</p>
|
||||
<MoneyField
|
||||
label="Durchschnittliches Bruttoeinkommen vor Planbeginn (real)"
|
||||
help="BRUTTO und 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."
|
||||
label="Durchschnittliches Bruttöinkommen vor Planbeginn (real)"
|
||||
help="BRUTTO und in HEUTIGER Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung übernehmen – dort ist die Aufwertung bereits enthalten. Die Rohbeträge 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."
|
||||
help="Jahre ohne AHV-Beiträge zwischen Alter 21 und Planbeginn (z. B. Ausland, Studium). Jedes kürzt die Rente um 1/44."
|
||||
value={gapBefore}
|
||||
min={0}
|
||||
max={career.yearsBeforePlan}
|
||||
@@ -206,12 +206,12 @@ export function AhvReviewFields({
|
||||
<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."
|
||||
help="Gewichtetes Mittel über 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."
|
||||
help="Vollrente zum mdJE (Skala 44), inkl. 13. Altersrente, gekürzt um die Ausfalljahre. Bei Ehepaaren zusätzlich plafoniert."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -220,7 +220,7 @@ export function AhvReviewFields({
|
||||
export function withTransitionDefaults(category: ElementCategory, isRetirement: boolean, td: TransitionData): TransitionData {
|
||||
const out = { ...td };
|
||||
if (category === "AHV") {
|
||||
// Ein Speichern der Pruefung markiert sie als erledigt (gleiches Muster wie bei PK/3a).
|
||||
// Ein Speichern der Prüfung 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";
|
||||
@@ -236,7 +236,7 @@ export function withTransitionDefaults(category: ElementCategory, isRetirement:
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Cash-Uebergang: einmalige Sonderein-/ausgaben ---
|
||||
// --- Cash-Übergang: einmalige Sonderein-/ausgaben ---
|
||||
|
||||
export function withCashTransitionDefaults(ct: CashTransitionData): CashTransitionData {
|
||||
return ct.mode === undefined ? { ...ct, mode: "NONE" } : { ...ct };
|
||||
@@ -246,7 +246,7 @@ export function isCashTransitionAnswered(ct: CashTransitionData): boolean {
|
||||
return ct.mode !== undefined;
|
||||
}
|
||||
|
||||
// Kurzfassung fuer die Uebergangszelle der Cash-Zeile.
|
||||
// Kurzfassung für die Übergangszelle der Cash-Zeile.
|
||||
export function cashTransitionSummary(ct: CashTransitionData): string {
|
||||
const mode = ct.mode;
|
||||
if (mode === undefined) return "?";
|
||||
@@ -264,7 +264,7 @@ export function cashTransitionSummary(ct: CashTransitionData): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Eingabefelder fuer den Cash-Entscheid. Erfassungs-Konventionen bewusst wie bei den
|
||||
// Eingabefelder für den Cash-Entscheid. Erfassungs-Konventionen bewusst wie bei den
|
||||
// laufenden Flows: Zufluss nominal (wie Einkommen), Kosten real (wie Ausgaben).
|
||||
export function CashTransitionFields({
|
||||
ct,
|
||||
@@ -287,8 +287,8 @@ export function CashTransitionFields({
|
||||
<>
|
||||
<div className="sm:col-span-2">
|
||||
<SelectField
|
||||
label="Entscheid fuer das Cash-Konto"
|
||||
help="Was passiert beim Uebergang in die naechste Lebensphase mit dem Cash-Bestand?"
|
||||
label="Entscheid für das Cash-Konto"
|
||||
help="Was passiert beim Übergang in die nächste Lebensphase mit dem Cash-Bestand?"
|
||||
value={mode}
|
||||
onChange={(v: CashTransitionMode) =>
|
||||
setC(
|
||||
@@ -302,7 +302,7 @@ export function CashTransitionFields({
|
||||
)
|
||||
}
|
||||
options={[
|
||||
{ value: "NONE", label: "1:1 uebernehmen" },
|
||||
{ value: "NONE", label: "1:1 übernehmen" },
|
||||
{ value: "INFLOW", label: "Einmaliger Zufluss" },
|
||||
{ value: "OUTFLOW", label: "Einmalige Kosten" },
|
||||
{ value: "BOTH", label: "Zufluss und Kosten" },
|
||||
@@ -314,7 +314,7 @@ export function CashTransitionFields({
|
||||
<>
|
||||
<div className="sm:col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
<strong className="text-fg">Einmaliger Zufluss</strong> (z. B. Erbschaft). Wird NOMINAL erfasst –
|
||||
der Betrag, der zu diesem Zeitpunkt tatsaechlich aufs Konto kommt.
|
||||
der Betrag, der zu diesem Zeitpunkt tatsächlich aufs Konto kommt.
|
||||
</div>
|
||||
<TextField
|
||||
label="Bezeichnung"
|
||||
@@ -337,13 +337,13 @@ export function CashTransitionFields({
|
||||
<DerivedField
|
||||
label="≈ real (heutige Kaufkraft)"
|
||||
value={Math.round(inflowNet / d)}
|
||||
help="Netto-Zufluss nach Steuer, zurueckgerechnet auf die Kaufkraft bei Planbeginn. Nur zur Info."
|
||||
help="Netto-Zufluss nach Steuer, zurückgerechnet auf die Kaufkraft bei Planbeginn. Nur zur Info."
|
||||
/>
|
||||
{taxRate > 0 && (
|
||||
<DerivedField
|
||||
label="Netto aufs Cash (nominal)"
|
||||
value={inflowNet}
|
||||
help="Betrag abzueglich Steuer. Wird automatisch berechnet."
|
||||
help="Betrag abzüglich Steuer. Wird automatisch berechnet."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -369,7 +369,7 @@ export function CashTransitionFields({
|
||||
<DerivedField
|
||||
label="≈ nominal (zum Zeitpunkt)"
|
||||
value={Math.round(num(ct.outflowAmount) * d)}
|
||||
help="Der Betrag, der zu diesem Zeitpunkt tatsaechlich faellig ist. Nur zur Info."
|
||||
help="Der Betrag, der zu diesem Zeitpunkt tatsächlich fällig ist. Nur zur Info."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -381,7 +381,7 @@ export function CashTransitionFields({
|
||||
export function isTransitionAnswered(category: ElementCategory, isRetirement: boolean, td: TransitionData): boolean {
|
||||
switch (category) {
|
||||
case "AHV":
|
||||
// Nur bei der Pensionierung ist eine Pruefung der Beitragskarriere noetig.
|
||||
// Nur bei der Pensionierung ist eine Prüfung der Beitragskarriere nötig.
|
||||
return isRetirement ? td.reviewed === true : true;
|
||||
case "REAL_ESTATE":
|
||||
case "OTHER_ASSET":
|
||||
@@ -395,7 +395,7 @@ export function isTransitionAnswered(category: ElementCategory, isRetirement: bo
|
||||
}
|
||||
}
|
||||
|
||||
// --- Wiederverwendbare Feldgruppen (Detail-Panel, Erstell-Popup, Uebergangs-Review) ---
|
||||
// --- Wiederverwendbare Feldgruppen (Detail-Panel, Erstell-Popup, Übergangs-Review) ---
|
||||
|
||||
export function ElementPhaseFields({
|
||||
element,
|
||||
@@ -414,7 +414,7 @@ export function ElementPhaseFields({
|
||||
case "EXPENSE": {
|
||||
const isIncome = element.category === "INCOME";
|
||||
// Basiswert (erstes Jahr). Ab Phase 2 mit dem fortgeschriebenen Wert der Vorphase
|
||||
// vorbelegt, aber bewusst aenderbar (Teilzeit, Befoerderung, Jobwechsel …).
|
||||
// vorbelegt, aber bewusst änderbar (Teilzeit, Beförderung, Jobwechsel …).
|
||||
const baseValue = typeof pd.amount === "number" ? pd.amount : carried ? context.derivedStart : 0;
|
||||
const rate = num(pd.teuerungsausgleich, 0);
|
||||
const d = context.deflatorStart || 1;
|
||||
@@ -424,12 +424,12 @@ export function ElementPhaseFields({
|
||||
<>
|
||||
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
{isIncome
|
||||
? "Bitte das NETTO-Einkommen erfassen: der Betrag, der nach allen Lohnabzuegen (AHV/ALV, Pensionskasse, NBU) tatsaechlich aufs Konto kommt – nicht der Bruttolohn. Erfasst wird NOMINAL; der reale Wert (heutige Kaufkraft) erscheint nur zur Info. Fuer die AHV-Rente rechnet das Tool intern auf den Bruttolohn zurueck."
|
||||
? "Bitte das NETTO-Einkommen erfassen: der Betrag, der nach allen Lohnabzügen (AHV/ALV, Pensionskasse, NBU) tatsächlich aufs Konto kommt – nicht der Bruttolohn. Erfasst wird NOMINAL; der reale Wert (heutige Kaufkraft) erscheint nur zur Info. Für die AHV-Rente rechnet das Tool intern auf den Bruttolohn zurück."
|
||||
: "Ausgaben werden REAL erfasst (in heutiger Kaufkraft). Die Inflation (plan-weit) rechnet daraus automatisch die nominalen Ausgaben – nur zur Info."}
|
||||
</p>
|
||||
<MoneyField
|
||||
label={isIncome ? "Jahreseinkommen NETTO, nominal (erstes Jahr)" : "Jahresausgaben REAL (erstes Jahr, heutige Kaufkraft)"}
|
||||
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
|
||||
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst änderbar." : undefined}
|
||||
value={baseValue}
|
||||
onChange={(v) => setP({ amount: v })}
|
||||
/>
|
||||
@@ -439,11 +439,11 @@ export function ElementPhaseFields({
|
||||
help="Nur zur Info, wird automatisch berechnet."
|
||||
/>
|
||||
<NumberField
|
||||
label={isIncome ? "Nominale Lohnerhoehung (%/Jahr)" : "Reale Mehrausgaben (%/Jahr)"}
|
||||
label={isIncome ? "Nominale Lohnerhöhung (%/Jahr)" : "Reale Mehrausgaben (%/Jahr)"}
|
||||
help={
|
||||
isIncome
|
||||
? "Geschaetzte jaehrliche Lohnerhoehung. 0% = Lohn bleibt nominal gleich und verliert real an Kaufkraft."
|
||||
: "Zusaetzliche reale Mehrausgaben pro Jahr (z. B. weil man sich mehr goennt). Die Inflation kommt separat automatisch dazu. 0% = gleicher Lebensstandard."
|
||||
? "Geschätzte jährliche Lohnerhöhung. 0% = Lohn bleibt nominal gleich und verliert real an Kaufkraft."
|
||||
: "Zusätzliche reale Mehrausgaben pro Jahr (z. B. weil man sich mehr gönnt). Die Inflation kommt separat automatisch dazu. 0% = gleicher Lebensstandard."
|
||||
}
|
||||
step={0.1}
|
||||
value={rate}
|
||||
@@ -454,25 +454,25 @@ 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).
|
||||
// Sonderfall: bei Planbeginn bereits pensioniert -> es gibt keinen Pensions-Übergang,
|
||||
// an dem die Karriere geprüft werden könnte. 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 –
|
||||
Diese Person ist bei Planbeginn bereits pensioniert. Die AHV-Rente hängt vom
|
||||
massgebenden durchschnittlichen Jahreseinkommen über die ganze Beitragsdauer ab –
|
||||
bitte hier erfassen (REAL, heutige Kaufkraft).
|
||||
</p>
|
||||
<MoneyField
|
||||
label="Durchschnittliches Bruttoeinkommen der Beitragsjahre (real)"
|
||||
help="BRUTTO und in heutiger Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung bzw. der Rentenverfuegung. Ab 90'720 gibt es die Maximalrente."
|
||||
label="Durchschnittliches Bruttöinkommen der Beitragsjahre (real)"
|
||||
help="BRUTTO und in heutiger Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung bzw. der Rentenverfügung. 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."
|
||||
help="Jahre ohne AHV-Beiträge. Jedes kürzt die Rente um 1/44."
|
||||
value={num(pd.gapYearsBefore)}
|
||||
min={0}
|
||||
onChange={(v) => setP({ gapYearsBefore: Math.max(0, Math.round(v)) })}
|
||||
@@ -480,14 +480,14 @@ export function ElementPhaseFields({
|
||||
<DerivedField
|
||||
label="Resultierende AHV-Rente pro Jahr"
|
||||
value={ahvAnnualPension(num(pd.avgIncomeBefore), num(pd.gapYearsBefore))}
|
||||
help="Inkl. 13. Altersrente. Bei Ehepaaren zusaetzlich plafoniert."
|
||||
help="Inkl. 13. Altersrente. Bei Ehepaaren zusätzlich plafoniert."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Die AHV-Rente wird aus der beim Pensions-Uebergang geprueften Beitragskarriere berechnet
|
||||
Die AHV-Rente wird aus der beim Pensions-Übergang geprüften Beitragskarriere berechnet
|
||||
(massgebendes Durchschnittseinkommen und Ausfalljahre). Bei Ehepaaren greift die
|
||||
Plafonierung auf 150% der Maximalrente.
|
||||
</p>
|
||||
@@ -496,7 +496,7 @@ export function ElementPhaseFields({
|
||||
return (
|
||||
<NumberField
|
||||
label="Geplante Ausfalljahre"
|
||||
help="Jahre ohne AHV-Beitraege in dieser Phase. Jedes Ausfalljahr kuerzt die spaetere Rente um 1/44."
|
||||
help="Jahre ohne AHV-Beiträge in dieser Phase. Jedes Ausfalljahr kürzt die spätere Rente um 1/44."
|
||||
value={num(pd.gapYears)}
|
||||
min={0}
|
||||
max={context.durationYears}
|
||||
@@ -507,8 +507,8 @@ export function ElementPhaseFields({
|
||||
if (!context.ownerWorking) {
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Die PK-Rente wird aus dem beim Pensions-Uebergang gewaehlten Umwandlungssatz berechnet (siehe
|
||||
Kennzahl). Bei reinem Kapitalbezug erscheint hier "Vollstaendig bezogen".
|
||||
Die PK-Rente wird aus dem beim Pensions-Übergang gewählten Umwandlungssatz berechnet (siehe
|
||||
Kennzahl). Bei reinem Kapitalbezug erscheint hier "Vollständig bezogen".
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -519,7 +519,7 @@ export function ElementPhaseFields({
|
||||
<DerivedField label="Startwert (fortgeschrieben)" value={context.derivedStart} help="Endwert der Vorphase, fortgeschrieben." />
|
||||
<MoneyField
|
||||
label="Zusatzeinlage aus Kapital (CHF)"
|
||||
help="Aufstockung aus dem verfuegbaren Kapital dieser Phase."
|
||||
help="Aufstockung aus dem verfügbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
@@ -529,8 +529,8 @@ export function ElementPhaseFields({
|
||||
<MoneyField label="Aktueller PK-Wert (CHF)" value={num(pd.currentValue)} onChange={(v) => setP({ currentValue: v })} />
|
||||
)}
|
||||
<MoneyField
|
||||
label="Jaehrliche Einzahlung (CHF)"
|
||||
help="Arbeitnehmer- und Arbeitgeberbeitraege. Fliesst NICHT in die Sparquote ein (bereits in den Ausgaben beruecksichtigt)."
|
||||
label="Jährliche Einzahlung (CHF)"
|
||||
help="Arbeitnehmer- und Arbeitgeberbeiträge. Fliesst NICHT in die Sparquote ein (bereits in den Ausgaben berücksichtigt)."
|
||||
value={num(pd.annualContribution)}
|
||||
onChange={(v) => setP({ annualContribution: v })}
|
||||
/>
|
||||
@@ -539,7 +539,7 @@ export function ElementPhaseFields({
|
||||
);
|
||||
case "PILLAR_3A":
|
||||
if (!context.ownerWorking) {
|
||||
return <p className="col-span-2 text-sm text-muted">Die Saeule 3a wird beim Pensions-Uebergang vollstaendig bezogen.</p>;
|
||||
return <p className="col-span-2 text-sm text-muted">Die Säule 3a wird beim Pensions-Übergang vollständig bezogen.</p>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -548,7 +548,7 @@ export function ElementPhaseFields({
|
||||
<DerivedField label="Startwert (fortgeschrieben)" value={context.derivedStart} help="Endwert der Vorphase, fortgeschrieben." />
|
||||
<MoneyField
|
||||
label="Zusatzeinlage aus Kapital (CHF)"
|
||||
help="Aufstockung aus dem verfuegbaren Kapital dieser Phase."
|
||||
help="Aufstockung aus dem verfügbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
@@ -558,8 +558,8 @@ export function ElementPhaseFields({
|
||||
<MoneyField label="Aktueller 3a-Wert (CHF)" value={num(pd.currentValue)} onChange={(v) => setP({ currentValue: v })} />
|
||||
)}
|
||||
<MoneyField
|
||||
label="Jaehrliche Einzahlung (CHF)"
|
||||
help={`Maximal CHF ${PILLAR_3A_MAX_ANNUAL.toLocaleString("de-CH")} (2026, mit PK) und hoechstens die Sparquote. Wird von der Sparquote abgezogen.`}
|
||||
label="Jährliche Einzahlung (CHF)"
|
||||
help={`Maximal CHF ${PILLAR_3A_MAX_ANNUAL.toLocaleString("de-CH")} (2026, mit PK) und höchstens die Sparquote. Wird von der Sparquote abgezogen.`}
|
||||
value={num(pd.annualContribution)}
|
||||
max={PILLAR_3A_MAX_ANNUAL}
|
||||
onChange={(v) => setP({ annualContribution: v })}
|
||||
@@ -594,7 +594,7 @@ export function ElementPhaseFields({
|
||||
)}
|
||||
<MoneyField
|
||||
label="Amortisation (CHF/Jahr)"
|
||||
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote und endet, sobald die Hypothek abbezahlt ist."
|
||||
help="Jährliche Reduktion der Hypothek. Zählt gegen die Sparquote und endet, sobald die Hypothek abbezahlt ist."
|
||||
value={num(pd.amortization)}
|
||||
onChange={(v) => setP({ amortization: v })}
|
||||
/>
|
||||
@@ -615,7 +615,7 @@ export function ElementPhaseFields({
|
||||
</div>
|
||||
</div>
|
||||
<NumberField
|
||||
label="Geschaetzte Wertsteigerung (%/Jahr)"
|
||||
label="Geschätzte Wertsteigerung (%/Jahr)"
|
||||
help="Wirkt auf den Wert der LIEGENSCHAFT, nicht auf das Eigenkapital. 1% von 1 Mio sind 10'000 pro Jahr – bei 100'000 Eigenkapital also 10% darauf (Hebel)."
|
||||
step={0.1}
|
||||
value={num(pd.valueGrowth)}
|
||||
@@ -624,12 +624,12 @@ export function ElementPhaseFields({
|
||||
<div className="sm:col-span-2">
|
||||
<SelectField
|
||||
label="Sind die Hypothekarzinsen bereits in den Ausgaben enthalten?"
|
||||
help="Damit die Zinsen nicht doppelt zaehlen. Standard: bereits enthalten – so verhalten sich bestehende Plaene unveraendert."
|
||||
help="Damit die Zinsen nicht doppelt zählen. Standard: bereits enthalten – so verhalten sich bestehende Pläne unverändert."
|
||||
value={handling}
|
||||
onChange={(v: "INCLUDED" | "ADD") => setP({ interestHandling: v })}
|
||||
options={[
|
||||
{ value: "INCLUDED", label: "Ja – bereits im Ausgaben-Element beruecksichtigt" },
|
||||
{ value: "ADD", label: "Nein – bitte zu den Ausgaben dazuzaehlen" },
|
||||
{ value: "INCLUDED", label: "Ja – bereits im Ausgaben-Element berücksichtigt" },
|
||||
{ value: "ADD", label: "Nein – bitte zu den Ausgaben dazuzählen" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -644,7 +644,7 @@ export function ElementPhaseFields({
|
||||
<DerivedField label="Startwert (fortgeschrieben)" value={context.derivedStart} help="Endwert der Vorphase, fortgeschrieben." />
|
||||
<MoneyField
|
||||
label="Zusatzinvestition aus Kapital (CHF)"
|
||||
help="Neuinvestition aus dem verfuegbaren Kapital dieser Phase."
|
||||
help="Neuinvestition aus dem verfügbaren Kapital dieser Phase."
|
||||
value={num(pd.additionalInvestment)}
|
||||
|
||||
onChange={(v) => setP({ additionalInvestment: v })}
|
||||
@@ -655,14 +655,14 @@ export function ElementPhaseFields({
|
||||
)}
|
||||
<NumberField label="Erwartete Rendite (%/Jahr)" step={0.1} value={num(pd.expectedReturn)} onChange={(v) => setP({ expectedReturn: v })} />
|
||||
<MoneyField
|
||||
label="Jaehrlicher Sparbeitrag (CHF)"
|
||||
help="Flacher Jahresbetrag, der ins Vermoegen fliesst und vom Cash abgezogen wird (geplante Sparrate)."
|
||||
label="Jährlicher Sparbeitrag (CHF)"
|
||||
help="Flacher Jahresbetrag, der ins Vermögen fliesst und vom Cash abgezogen wird (geplante Sparrate)."
|
||||
value={num(pd.annualContribution)}
|
||||
onChange={(v) => setP({ annualContribution: v })}
|
||||
/>
|
||||
<MoneyField
|
||||
label="Jaehrliche Bezugsrate (CHF)"
|
||||
help="Entnahme aus dem Vermoegen (z. B. laufende Renten-Entnahme im Alter). Mindert das Vermoegen und fliesst jaehrlich ins Cash (geplante Verzehrrate)."
|
||||
label="Jährliche Bezugsrate (CHF)"
|
||||
help="Entnahme aus dem Vermögen (z. B. laufende Renten-Entnahme im Alter). Mindert das Vermögen und fliesst jährlich ins Cash (geplante Verzehrrate)."
|
||||
value={num(pd.annualWithdrawal)}
|
||||
onChange={(v) => setP({ annualWithdrawal: v })}
|
||||
/>
|
||||
@@ -677,8 +677,8 @@ export function ElementPhaseFields({
|
||||
<MoneyField label="Restschuld (CHF)" value={num(pd.startValue)} onChange={(v) => setP({ startValue: v })} />
|
||||
)}
|
||||
<MoneyField
|
||||
label="Jaehrliche Tilgung (CHF)"
|
||||
help="Zaehlt gegen die Sparquote (max. die Sparquote)."
|
||||
label="Jährliche Tilgung (CHF)"
|
||||
help="Zählt gegen die Sparquote (max. die Sparquote)."
|
||||
value={num(pd.annualRepayment)}
|
||||
|
||||
onChange={(v) => setP({ annualRepayment: v })}
|
||||
@@ -704,8 +704,8 @@ export function ElementTransitionFields({
|
||||
case "EXPENSE":
|
||||
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.
|
||||
Für diese Kategorie gibt es im Übergang keine Eingaben. Die Werte werden 1:1 in die nächste
|
||||
Lebensphase übernommen und können dort angepasst werden.
|
||||
</p>
|
||||
);
|
||||
case "AHV":
|
||||
@@ -714,7 +714,7 @@ export function ElementTransitionFields({
|
||||
}
|
||||
return (
|
||||
<p className="col-span-2 text-sm text-muted">
|
||||
Die Beitragskarriere wird erst beim Uebergang in die Pensionierung geprueft.
|
||||
Die Beitragskarriere wird erst beim Übergang in die Pensionierung geprüft.
|
||||
</p>
|
||||
);
|
||||
case "PENSION_FUND":
|
||||
@@ -735,7 +735,7 @@ export function ElementTransitionFields({
|
||||
{(mode === "PENSION" || mode === "COMBI") && (
|
||||
<NumberField
|
||||
label="Umwandlungssatz (%)"
|
||||
help="Jaehrliche Rente = verrentetes Kapital x Umwandlungssatz."
|
||||
help="Jährliche Rente = verrentetes Kapital x Umwandlungssatz."
|
||||
step={0.1}
|
||||
value={num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)}
|
||||
onChange={(v) => setT({ conversionRate: v })}
|
||||
@@ -762,7 +762,7 @@ export function ElementTransitionFields({
|
||||
return (
|
||||
<NumberField
|
||||
label="Kapitalbezugssteuer (%)"
|
||||
help="Die Saeule 3a wird bei Pensionierung vollstaendig bezogen."
|
||||
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 })}
|
||||
@@ -775,7 +775,7 @@ export function ElementTransitionFields({
|
||||
const marktwert = Math.round(context.propertyValueEnd);
|
||||
const preis = num(td.salePrice);
|
||||
// Abweichung zwischen erfasstem Verkaufspreis und modelliertem Verkehrswert. Beide
|
||||
// Groessen sind unabhaengig erfassbar -- ohne diesen Vergleich koennte man 2 %
|
||||
// Grössen sind unabhängig erfassbar -- ohne diesen Vergleich könnte man 2 %
|
||||
// Wertsteigerung annehmen und trotzdem zum Kaufpreis verkaufen, ohne es zu merken.
|
||||
const abweichung = preis - marktwert;
|
||||
const abweichungPct = marktwert > 0 ? (abweichung / marktwert) * 100 : 0;
|
||||
@@ -833,13 +833,13 @@ export function ElementTransitionFields({
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<NumberField label="Grundstueckgewinnsteuer (%)" step={1} value={num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE)} onChange={(v) => setT({ saleTaxRate: v })} />
|
||||
<NumberField label="Grundstückgewinnsteuer (%)" step={1} value={num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE)} onChange={(v) => setT({ saleTaxRate: v })} />
|
||||
</>
|
||||
)}
|
||||
{decision !== "SELL" && context.mortgageEnd > 0 && (
|
||||
<MoneyField
|
||||
label="Sonderamortisation (CHF)"
|
||||
help={`Einmaltilgung der Hypothek aus dem Cash der naechsten Phase (z. B. aus einem 3a-Bezug). Senkt Restschuld und kuenftige Zinsen. Maximal ${formatChf(context.mortgageEnd)} (Resthypothek am Phasenende).`}
|
||||
help={`Einmaltilgung der Hypothek aus dem Cash der nächsten Phase (z. B. aus einem 3a-Bezug). Senkt Restschuld und künftige Zinsen. Maximal ${formatChf(context.mortgageEnd)} (Resthypothek am Phasenende).`}
|
||||
value={num(td.extraAmortization)}
|
||||
max={context.mortgageEnd}
|
||||
onChange={(v) => setT({ extraAmortization: v })}
|
||||
@@ -865,7 +865,7 @@ export function ElementTransitionFields({
|
||||
{decision === "PARTIAL" && (
|
||||
<MoneyField
|
||||
label="Teilverkauf (CHF)"
|
||||
help={`Betrag, der am Uebergang ins Cash fliesst. Der Rest bleibt investiert. Maximal ${formatChf(context.carriedEndValue)} (Endwert der Vorphase).`}
|
||||
help={`Betrag, der am Übergang ins Cash fliesst. Der Rest bleibt investiert. Maximal ${formatChf(context.carriedEndValue)} (Endwert der Vorphase).`}
|
||||
value={num(td.partialSaleAmount)}
|
||||
max={context.carriedEndValue}
|
||||
onChange={(v) => setT({ partialSaleAmount: v })}
|
||||
@@ -878,7 +878,7 @@ export function ElementTransitionFields({
|
||||
return (
|
||||
<MoneyField
|
||||
label="Sofortige Tilgung (CHF)"
|
||||
help="Wird sofort getilgt und vom verfuegbaren Kapital der naechsten Phase abgezogen."
|
||||
help="Wird sofort getilgt und vom verfügbaren Kapital der nächsten Phase abgezogen."
|
||||
value={num(td.immediateRepayment)}
|
||||
onChange={(v) => setT({ immediateRepayment: v })}
|
||||
/>
|
||||
@@ -903,8 +903,8 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
} else {
|
||||
const payload: PhaseData = { ...pd };
|
||||
// Einkommen/Ausgaben ab Phase 2: entspricht der Basiswert dem fortgeschriebenen Wert
|
||||
// der Vorphase, KEINEN Override speichern -> Wert bleibt live vererbt (Aenderungen in
|
||||
// frueheren Phasen wirken sich weiter aus). Nur ein bewusst abweichender Wert wird fix.
|
||||
// der Vorphase, KEINEN Override speichern -> Wert bleibt live vererbt (Änderungen in
|
||||
// früheren Phasen wirken sich weiter aus). Nur ein bewusst abweichender Wert wird fix.
|
||||
if (
|
||||
(element.category === "INCOME" || element.category === "EXPENSE") &&
|
||||
context.carried &&
|
||||
@@ -936,7 +936,7 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-accent-soft-fg">
|
||||
{CATEGORY_LABELS[element.category]}
|
||||
{isTransition ? " · Uebergang" : ""}
|
||||
{isTransition ? " · Übergang" : ""}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-fg">{element.name}</div>
|
||||
</div>
|
||||
@@ -945,12 +945,12 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS
|
||||
onClick={onDeleteElement}
|
||||
className="flex items-center gap-1 rounded-lg border border-border px-2 py-1 text-xs text-muted hover:border-danger hover:bg-danger-soft hover:text-danger"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> Element loeschen
|
||||
<Trash2 className="h-3.5 w-3.5" /> Element löschen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{/* Punkt 4: Warnung, dass Aenderungen sich in die Folgephasen fortschreiben. */}
|
||||
{/* Punkt 4: Warnung, dass Änderungen 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