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
+50 -2
View File
@@ -8,6 +8,7 @@ import {
Dices,
FileText,
FolderKanban,
CalendarClock,
GitBranch,
History,
LayoutDashboard,
@@ -26,6 +27,9 @@ import { Dashboard } from "@/components/Dashboard";
import { MonteCarloDialog } from "@/components/MonteCarloDialog";
import { SensitivityDialog } from "@/components/SensitivityDialog";
import { LiveSimDialog } from "@/components/LiveSimDialog";
import { ActualsDialog } from "@/components/ActualsDialog";
import { buildViews } from "@/lib/dataview";
import type { ActualsSetInput, ElementOrigin } from "@/lib/actuals";
import { VersionHistoryDialog } from "@/components/VersionHistoryDialog";
import { SpecView } from "@/components/SpecView";
import { SystemParametersView } from "@/components/SystemParametersView";
@@ -56,6 +60,10 @@ interface ScenarioDetail {
computed: PlanComputed;
base: PlanInput | null; // Eltern-Szenario als Vergleichsbasis
meta: ScenarioMeta & { planName: string };
// Effektive Werte des PLANS plus die Element-Herkunft aller Szenarien -- daraus entsteht
// im Browser der zweite Rechenlauf (siehe lib/dataview.ts).
actuals: ActualsSetInput[];
elementOrigins: ElementOrigin[];
}
// Die Provider (Toast, Bestätigung) müssen UM die Shell liegen, damit deren Hooks
@@ -90,6 +98,7 @@ function AppShellInner({ username }: { username: string }) {
const [showSensitivity, setShowSensitivity] = useState(false);
const [showLiveSim, setShowLiveSim] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const [showActuals, setShowActuals] = useState(false);
const [showSystemParams, setShowSystemParams] = useState(false);
const [showPlanTraces, setShowPlanTraces] = useState(false);
const [showPalette, setShowPalette] = useState(false);
@@ -210,6 +219,14 @@ function AppShellInner({ username }: { username: string }) {
const activePlan = plans.find((p) => p.scenarios.some((s) => s.id === selectedScenarioId)) ?? null;
// Plan-Sicht und Ist-Sicht in einem Zug. Ohne erfasste Ist-Werte bleibt `actual` null und
// die Oberflaeche verhaelt sich exakt wie bisher.
const views = useMemo(
() =>
detail ? buildViews(detail.plan, detail.actuals ?? [], detail.elementOrigins ?? []) : null,
[detail]
);
// Aktionen der Befehls-Palette -- kontextabhängig (Analysen nur bei offenem Szenario).
const paletteActions = useMemo<PaletteAction[]>(() => {
const base: PaletteAction[] = [
@@ -426,6 +443,10 @@ function AppShellInner({ username }: { username: string }) {
<BarChart3 className="h-4 w-4" />
Grafiken
</Button>
<Button variant="secondary" onClick={() => setShowActuals(true)}>
<CalendarClock className="h-4 w-4" />
Effektive Werte
</Button>
<Button variant="secondary" onClick={() => setShowHistory(true)}>
<History className="h-4 w-4" />
Änderungshistorie
@@ -465,6 +486,8 @@ function AppShellInner({ username }: { username: string }) {
<PlanView
plan={detail.plan}
computed={detail.computed}
actualComputed={views?.actual ?? null}
actualYears={views?.actualYears ?? []}
diff={diff}
onChanged={refreshCurrent}
onOpenSpec={openSpecAt}
@@ -525,6 +548,8 @@ function AppShellInner({ username }: { username: string }) {
plan={detail.plan}
computed={detail.computed}
siblings={(activePlan?.scenarios ?? []).filter((s) => s.id !== detail.meta.id)}
actuals={detail.actuals ?? []}
origins={detail.elementOrigins ?? []}
/>
</ChartsDialog>
)}
@@ -535,16 +560,39 @@ function AppShellInner({ username }: { username: string }) {
computed={detail.computed}
meta={detail.meta}
scenarios={activePlan?.scenarios ?? [detail.meta]}
actuals={detail.actuals ?? []}
origins={detail.elementOrigins ?? []}
onClose={() => setShowMonteCarlo(false)}
/>
)}
{showSensitivity && detail && (
<SensitivityDialog plan={detail.plan} onClose={() => setShowSensitivity(false)} />
<SensitivityDialog
plan={detail.plan}
actuals={detail.actuals ?? []}
origins={detail.elementOrigins ?? []}
onClose={() => setShowSensitivity(false)}
/>
)}
{showLiveSim && detail && (
<LiveSimDialog plan={detail.plan} onClose={() => setShowLiveSim(false)} />
<LiveSimDialog
plan={detail.plan}
actuals={detail.actuals ?? []}
origins={detail.elementOrigins ?? []}
onClose={() => setShowLiveSim(false)}
/>
)}
{showActuals && detail && activePlan && (
<ActualsDialog
planId={detail.meta.planId}
planName={detail.meta.planName}
scenarioMetas={activePlan.scenarios}
initial={{ id: detail.meta.id, name: detail.meta.name, isBase: detail.meta.isBase, plan: detail.plan }}
onClose={() => setShowActuals(false)}
onChanged={refreshCurrent}
/>
)}
{showHistory && detail && (