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
+70 -8
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import {
BarChart3,
BookOpen,
Copy,
Dices,
FileText,
@@ -12,6 +13,7 @@ import {
Menu,
PiggyBank,
Plus,
SlidersHorizontal,
Tornado,
Trash2,
X,
@@ -21,6 +23,9 @@ import { Dashboard } from "@/components/Dashboard";
import { MonteCarloDialog } from "@/components/MonteCarloDialog";
import { SensitivityDialog } from "@/components/SensitivityDialog";
import { SpecView } from "@/components/SpecView";
import { SystemParametersView } from "@/components/SystemParametersView";
import { PlanTraceDialog } from "@/components/DetailView";
import { computePlan } from "@/lib/calculations";
import { ProfileMenu } from "@/components/ProfileMenu";
import { PlanProfileFields, emptyProfileDraft, type ProfileDraft } from "@/components/PlanProfileFields";
import { api } from "@/lib/api-client";
@@ -47,6 +52,17 @@ export function AppShell({ username }: { username: string }) {
const [showCharts, setShowCharts] = useState(false);
const [showMonteCarlo, setShowMonteCarlo] = useState(false);
const [showSensitivity, setShowSensitivity] = useState(false);
const [showSystemParams, setShowSystemParams] = useState(false);
const [showPlanTraces, setShowPlanTraces] = useState(false);
// Sprungmarke in die SPEZIFIKATION, gesetzt aus einem Rechenweg heraus.
const [specAnchor, setSpecAnchor] = useState<string | null>(null);
function openSpecAt(anchor: string) {
setSpecAnchor(anchor);
setShowSpec(true);
setShowSystemParams(false);
setSelectedScenarioId(null);
}
const loadPlans = useCallback(async () => {
const data = await api.get<{ plans: PlanListItem[] }>("/api/plans");
@@ -86,6 +102,7 @@ export function AppShell({ username }: { username: string }) {
function openScenario(id: string) {
setSelectedScenarioId(id);
setShowSpec(false);
setShowSystemParams(false);
setSidebarOpen(false);
}
@@ -127,10 +144,11 @@ export function AppShell({ username }: { username: string }) {
onClick={() => {
setSelectedScenarioId(null);
setShowSpec(false);
setShowSystemParams(false);
setSidebarOpen(false);
}}
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium ${
selectedScenarioId === null && !showSpec ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
selectedScenarioId === null && !showSpec && !showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
}`}
>
<LayoutDashboard className="h-4 w-4" />
@@ -176,11 +194,28 @@ export function AppShell({ username }: { username: string }) {
</div>
))}
<div className="mt-4 border-t border-border pt-3">
<div className="mt-4 flex flex-col gap-1 border-t border-border pt-3">
<button
type="button"
onClick={() => {
setShowSystemParams(true);
setShowSpec(false);
setSelectedScenarioId(null);
setSidebarOpen(false);
}}
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium ${
showSystemParams ? "bg-accent-soft text-accent-soft-fg" : "text-muted hover:bg-surface-2"
}`}
>
<SlidersHorizontal className="h-4 w-4 shrink-0" />
Systemparameter
</button>
<button
type="button"
onClick={() => {
setShowSpec(true);
setSpecAnchor(null);
setShowSystemParams(false);
setSelectedScenarioId(null);
setSidebarOpen(false);
}}
@@ -230,7 +265,9 @@ export function AppShell({ username }: { username: string }) {
<Menu className="h-4 w-4" />
</button>
<h1 className="min-w-0 flex-1 truncate text-base font-semibold text-fg">
{showSpec
{showSystemParams
? "Systemparameter"
: showSpec
? "Spezifikation"
: detail
? `${detail.meta.planName} · ${detail.meta.name}`
@@ -240,11 +277,13 @@ export function AppShell({ username }: { username: string }) {
</header>
<main className="flex-1 px-4 py-6 lg:px-8">
{showSpec && <SpecView />}
{showSystemParams && <SystemParametersView />}
{!showSpec && loading && <p className="text-sm text-muted">Laedt</p>}
{showSpec && <SpecView anchor={specAnchor} />}
{!showSpec && !loading && selectedScenarioId === null && (
{!showSpec && !showSystemParams && loading && <p className="text-sm text-muted">Laedt</p>}
{!showSpec && !showSystemParams && !loading && selectedScenarioId === null && (
<DashboardHome
username={username}
plans={plans}
@@ -254,7 +293,7 @@ export function AppShell({ username }: { username: string }) {
/>
)}
{!showSpec && !loading && detail && selectedScenarioId && (
{!showSpec && !showSystemParams && !loading && detail && selectedScenarioId && (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center gap-2">
<button
@@ -301,6 +340,15 @@ export function AppShell({ username }: { username: string }) {
<Tornado className="h-4 w-4" />
Einflussfaktoren berechnen
</button>
<button
type="button"
onClick={() => setShowPlanTraces(true)}
title="Wie wird gerechnet? Plan-weite Grössen wie Deflatoren, AHV-Karriere und Ruinalter"
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-muted hover:bg-surface-2"
>
<BookOpen className="h-4 w-4" />
Rechenwege
</button>
</>
)}
{diff && detail.base && (
@@ -313,7 +361,13 @@ export function AppShell({ username }: { username: string }) {
)}
</div>
<PlanView plan={detail.plan} computed={detail.computed} diff={diff} onChanged={refreshCurrent} />
<PlanView
plan={detail.plan}
computed={detail.computed}
diff={diff}
onChanged={refreshCurrent}
onOpenSpec={openSpecAt}
/>
</div>
)}
</main>
@@ -358,6 +412,14 @@ export function AppShell({ username }: { username: string }) {
<SensitivityDialog plan={detail.plan} onClose={() => setShowSensitivity(false)} />
)}
{showPlanTraces && detail && (
<PlanTraceDialog
computed={computePlan(detail.plan, undefined, { explain: true })}
onClose={() => setShowPlanTraces(false)}
onOpenSpec={openSpecAt}
/>
)}
{copyFrom && (
<CopyScenarioDialog
source={copyFrom}