UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
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:
2026-07-19 07:40:42 +02:00
parent e1f74fca95
commit c2fb82b0de
60 changed files with 2915 additions and 1210 deletions
+91 -91
View File
@@ -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 &quot;Vollstaendig bezogen&quot;.
Die PK-Rente wird aus dem beim Pensions-Übergang gewählten Umwandlungssatz berechnet (siehe
Kennzahl). Bei reinem Kapitalbezug erscheint hier &quot;Vollständig bezogen&quot;.
</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} />