Detailansichten, Wasserfaelle und vollstaendige Rechenweg-Offenlegung
Deploy App / deploy (push) Successful in 57s

Roadmap Nr. 43 (Detailansichten) und Nr. 41 (Berechnungslogiken offenlegen).

Systemparameter-Ansicht:
- SYSTEM_PARAMETERS in constants.ts: Wert, Bedeutung, Herleitung, Quelle,
  Stand -- aus derselben Datei, aus der gerechnet wird

Detailansichten (nur lesen, per Expand-Icon):
- Element: Verlauf ueber ALLE Planjahre (neu ElementPhaseComputed.yearly),
  bei Immobilien Verkehrswert/Restschuld/Eigenkapital getrennt
- Phase: Vermoegensaufteilung + zwei Wasserfaelle

Zwei getrennte Wasserfaelle (WealthBridge / CashBridge):
- Sparraten, Amortisationen und Investitionen sind UMBUCHUNGEN und erscheinen
  nur im Cash-Wasserfall -- als Vermoegensabgang gezeichnet wuerden sie einen
  Verlust vortaeuschen, den es nicht gibt
- PK-Beitraege dagegen sind ein echter Vermoegenszugang (belasten kein Cash),
  Verrentung ein echter Abgang (Kapital verlaesst die Bilanz)
- residual als Kontrollgroesse fuer die Vollstaendigkeit der Zerlegung

Rechenweg-Protokoll, vollstaendige Abdeckung:
- computePlan(plan, sample?, { explain }) protokolliert die Schritte, die es
  ohnehin ausfuehrt -- die Erklaerung IST die Rechnung, statt einer zweiten
  Formel-Implementierung im UI, die still abdriften koennte
- standardmaessig aus (Monte Carlo bleibt unberuehrt)
- Arithmetik nicht umgestellt: Zwischengroessen werden als Differenz
  abgeleitet, damit die 43 Golden Tests bitgleich bleiben
- jeder Trace verlinkt in die SPEZIFIKATION; ein Test prueft gegen die echte
  Datei, dass alle Verweise eine existierende Ueberschrift treffen

SPEZIFIKATION auf 0.11: neue Kapitel 3.6.7, 3.6.8, 4.14, 9.20, 9.21.
12 Tests ergaenzt (80 -> 92). Keine DB-Aenderung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 17:12:55 +02:00
parent 1836cad7f3
commit 4791dccf93
9 changed files with 2066 additions and 33 deletions
+92
View File
@@ -0,0 +1,92 @@
"use client";
import { SlidersHorizontal } from "lucide-react";
import { formatChf } from "@/lib/format";
import { SYSTEM_PARAMETERS, type ParameterGroup, type SystemParameter } from "@/lib/constants";
const GROUPS: { key: ParameterGroup; title: string; intro: string }[] = [
{
key: "AHV",
title: "AHV",
intro:
"Diese Werte legt der Bund fest. Sie ändern sich periodisch der Stand ist je Parameter ausgewiesen.",
},
{
key: "Vorsorge",
title: "Berufliche und gebundene Vorsorge",
intro: "Höchstbeträge und Umwandlung. Der Umwandlungssatz ist ein Vorschlag und pro Ereignis überschreibbar.",
},
{
key: "Steuern",
title: "Steuersätze",
intro:
"Bewusst pauschale Vorschlagswerte, keine Steuerberechnung: Die tatsächlichen Sätze sind kantonal, progressiv und vom Einzelfall abhängig. Sie lassen sich pro Ereignis überschreiben.",
},
];
function formatValue(p: SystemParameter): string {
switch (p.unit) {
case "CHF":
return `${formatChf(p.value)} CHF`;
case "%":
return `${p.value} %`;
case "Jahre":
return `${p.value} Jahre`;
default:
return String(p.value);
}
}
// Zeigt die Systemparameter mit Wert, Bedeutung, Herleitung und Quelle (Roadmap Nr. 41).
// Die Eintraege stammen direkt aus constants.ts -- dieselbe Datenquelle, aus der auch
// gerechnet wird. Ein Abdriften zwischen Anzeige und Rechnung ist damit ausgeschlossen.
export function SystemParametersView() {
return (
<div className="flex flex-col gap-5">
<div className="flex items-center gap-2">
<SlidersHorizontal className="h-5 w-5 text-accent" />
<h2 className="text-lg font-semibold text-fg">Systemparameter</h2>
</div>
<p className="max-w-3xl text-sm text-muted">
Alle fest hinterlegten Grössen, mit denen das Tool rechnet inklusive Herleitung und Quelle. Sie stammen aus
derselben Datei, aus der auch die Berechnung liest; was hier steht, ist also garantiert das, was gerechnet wird.
</p>
{GROUPS.map((g) => {
const rows = SYSTEM_PARAMETERS.filter((p) => p.group === g.key);
if (rows.length === 0) return null;
return (
<section key={g.key} className="rounded-xl border border-border bg-surface p-4 shadow-sm">
<h3 className="text-sm font-semibold text-fg">{g.title}</h3>
<p className="mb-3 mt-0.5 text-xs text-muted">{g.intro}</p>
<div className="flex flex-col gap-3">
{rows.map((p) => (
<div key={p.key} className="rounded-lg border border-border bg-surface-2 p-3">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="text-sm font-medium text-fg">{p.label}</span>
<span className="text-sm font-semibold text-accent">{formatValue(p)}</span>
</div>
<p className="mt-1 text-xs text-muted">{p.meaning}</p>
{p.derivation && (
<p className="mt-1 font-mono text-[11px] text-faint">Herleitung: {p.derivation}</p>
)}
<div className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-faint">
<span>Quelle: {p.source}</span>
<span>Stand: {p.validFrom}</span>
{p.editablePerEvent && <span className="text-accent">pro Ereignis überschreibbar</span>}
</div>
</div>
))}
</div>
</section>
);
})}
<p className="max-w-3xl text-xs text-faint">
Die drei Steuersätze werden sowohl als Vorschlag im Eingabefeld als auch in der Berechnung als Rückfallwert
verwendet damit ein nicht angetippter Wert nicht fälschlich als 0 % gerechnet wird.
</p>
</div>
);
}