Effektive Werte: Plan-Ist-Vergleich (Roadmap 5)
Deploy App / deploy (push) Successful in 1m57s

Neuer Knopf auf Plan-Ebene: Liste plus Wizard in zwei Schritten. Ein
Ist-Satz haengt am PLAN, nicht am Szenario -- die Zuordnung laeuft ueber
die Herkunfts-Kette sourceElementId.

computePlan nimmt neu { actuals }: Die Werte schnappen in jedem erfassten
Jahr auf die Realitaet und laufen von dort planmaessig weiter. Luecken
fallen auf die Plandaten zurueck. Ohne die Option unveraendert -- die 43
Golden Tests laufen durch.

Der Sprung ist keine Rendite: eigene Brueckenposition actualsCorrection
in Vermoegens- und Cash-Bruecke, sonst ginge die Zerlegung nicht auf.

Matrix: Umschalter Plan/Effektiv, im Ist-Modus mit farbiger Abweichung
statt acht Zahlen je Zelle. Zeitachse: Marker je Jahr, juengster farbig.

Vier Analysewerkzeuge mit einheitlicher Leiste (nominal/real als
Einfachauswahl, Plan/Effektiv). MC: Zielbetrag dreht mit, Startjahr
abgeleitet statt eingebbar.

Neue Tabelle ActualsSet (gegen echtes Postgres verifiziert), Module
actuals.ts und dataview.ts. Spezifikation 0.21, 27 Tests (181 -> 208).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:35:07 +02:00
parent 4343d50aa4
commit e117e5a4e5
25 changed files with 1991 additions and 74 deletions
+22 -20
View File
@@ -3,9 +3,11 @@
import { useMemo, useState } from "react";
import { Bar, BarChart, CartesianGrid, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Tornado, X } from "lucide-react";
import { RequiredNumberField, SelectField } from "@/components/FormField";
import { RequiredNumberField } from "@/components/FormField";
import { InfoBubble } from "@/components/InfoBubble";
import { useVersionedPlan, VersionBar } from "@/components/VersionPicker";
import { AnalysisBar, useAnalysisBasis } from "@/components/AnalysisControls";
import type { ActualsSetInput, ElementOrigin } from "@/lib/actuals";
import { formatChf } from "@/lib/format";
import {
computeTornado,
@@ -35,12 +37,23 @@ function formatRange(low: number, high: number, unit: DriverDef["unit"]): string
return `${sign(low)} ${suffix}${sign(high)} ${suffix}`;
}
export function SensitivityDialog({ plan: currentPlan, onClose }: { plan: PlanInput; onClose: () => void }) {
export function SensitivityDialog({
plan: currentPlan,
actuals = [],
origins = [],
onClose,
}: {
plan: PlanInput;
actuals?: ActualsSetInput[];
origins?: ElementOrigin[];
onClose: () => void;
}) {
// Gerechnet wird wahlweise auf dem Arbeitsstand oder auf einer festgehaltenen Version.
const { versionId, setVersionId, plan, loading: versionLoading } = useVersionedPlan(currentPlan.id, currentPlan);
const available = useMemo(() => DRIVERS.filter((d) => d.applies(plan)), [plan]);
const [metric, setMetric] = useState<TornadoMetric>("real");
const basis = useAnalysisBasis(plan, actuals, origins, "real");
const metric: TornadoMetric = basis.metric;
const [drafts, setDrafts] = useState<Record<string, RangeDraft>>({});
const [result, setResult] = useState<TornadoResult | null>(null);
@@ -75,7 +88,10 @@ export function SensitivityDialog({ plan: currentPlan, onClose }: { plan: PlanIn
checked.map((d) => {
const dr = draftFor(d.id);
return { id: d.id, low: Number(dr.low), high: Number(dr.high) };
})
}),
// Mit Ist-Werten wirken die Treiber nur noch auf die NICHT belegten Jahre -- was
// erfasst ist, steht fest. Die Balken fallen dadurch zu Recht kuerzer aus.
basis.resolvedActuals
)
);
}
@@ -143,22 +159,8 @@ export function SensitivityDialog({ plan: currentPlan, onClose }: { plan: PlanIn
</p>
</div>
{/* Zielgrösse */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<SelectField
label="Zielgrösse"
help="Woran die Wirkung gemessen wird: das Endvermögen der letzten Lebensphase. «Real» ist kaufkraftbereinigt auf den Planbeginn und damit die ehrlichere Grösse."
value={metric}
onChange={(v: TornadoMetric) => {
setMetric(v);
setResult(null);
}}
options={[
{ value: "real", label: "Endvermögen real (kaufkraftbereinigt)" },
{ value: "nominal", label: "Endvermögen nominal" },
]}
/>
</div>
{/* Zielgrösse und Datengrundlage */}
<AnalysisBar basis={basis} onChange={() => setResult(null)} />
{/* Parameter */}
<div>