Vererbung Einkommen/Ausgaben + Real/Nominal-Umschalter
Deploy App / deploy (push) Successful in 1m2s
Deploy App / deploy (push) Successful in 1m2s
Punkt 1: Beim Speichern eines Einkommens-/Ausgaben-Basiswerts, der dem fortgeschriebenen Wert der Vorphase entspricht, wird KEIN Override gespeichert. Damit wirken sich Aenderungen in frueheren Phasen automatisch auf Folgephasen aus (live vererbt); nur ein bewusst abweichender Wert bleibt fix. Punkt 2: Anzeige-Umschalter oben links (Nominal / Beide / Real), pro Geraet gespeichert. Alle Betraege in Matrix, Cash-Zeile und Phasenkopf zeigen je nach Wahl den nominalen, realen (kaufkraftbereinigten) oder beide Werte. Engine liefert dazu cumulativeInflationStart (Deflator zu Phasenbeginn) zusaetzlich zum Phasenende. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+69
-11
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
@@ -96,6 +96,17 @@ export function PlanView({
|
||||
const [editTransition, setEditTransition] = useState<{ elementId: string; fromPhaseId: string } | null>(null);
|
||||
const [editPhaseCell, setEditPhaseCell] = useState<{ elementId: string; phaseId: string } | null>(null);
|
||||
const [showCashInit, setShowCashInit] = useState(false);
|
||||
const [valueMode, setValueMode] = useState<ValueMode>("nominal");
|
||||
|
||||
useEffect(() => {
|
||||
const stored = typeof window !== "undefined" ? window.localStorage.getItem(VALUE_MODE_KEY) : null;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- einmalige Uebernahme der gespeicherten Wahl
|
||||
if (stored === "nominal" || stored === "both" || stored === "real") setValueMode(stored);
|
||||
}, []);
|
||||
function changeValueMode(m: ValueMode) {
|
||||
setValueMode(m);
|
||||
if (typeof window !== "undefined") window.localStorage.setItem(VALUE_MODE_KEY, m);
|
||||
}
|
||||
|
||||
const columns = useMemo<Column[]>(() => {
|
||||
const cols: Column[] = [];
|
||||
@@ -239,6 +250,25 @@ export function PlanView({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted">Anzeige</span>
|
||||
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
|
||||
{(["nominal", "both", "real"] as ValueMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => changeValueMode(m)}
|
||||
className={`rounded-md px-2.5 py-1 font-medium ${
|
||||
valueMode === m ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{m === "nominal" ? "Nominal" : m === "both" ? "Beide" : "Real"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[11px] text-faint">real = kaufkraftbereinigt (Planbeginn)</span>
|
||||
</div>
|
||||
|
||||
<Timeline phases={computed.phases} persons={personAxes} ruinAge={computed.ruinAge} />
|
||||
|
||||
{/* Plan-Profil */}
|
||||
@@ -312,6 +342,7 @@ export function PlanView({
|
||||
key={col.phase.id}
|
||||
phase={col.phase}
|
||||
personLabel={personLabel}
|
||||
mode={valueMode}
|
||||
onClick={() => setSelected({ type: "phase", phaseId: col.phase.id })}
|
||||
active={selected?.type === "phase" && selected.phaseId === col.phase.id}
|
||||
/>
|
||||
@@ -346,7 +377,9 @@ export function PlanView({
|
||||
} ${col.phase.cashNegative ? "font-semibold text-danger" : "text-fg"}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">
|
||||
{formatChf(col.phase.cashStart)} <span className="text-faint">→</span> {formatChf(col.phase.cashEnd)}
|
||||
{valStr(col.phase.cashStart, col.phase.cumulativeInflationStart, valueMode)}{" "}
|
||||
<span className="text-faint">→</span>{" "}
|
||||
{valStr(col.phase.cashEnd, col.phase.cumulativeInflationEnd, valueMode)}
|
||||
</span>
|
||||
</td>
|
||||
) : (
|
||||
@@ -402,7 +435,7 @@ export function PlanView({
|
||||
ce?.locked ? "text-faint" : "text-fg"
|
||||
}`}
|
||||
>
|
||||
{phaseCellContent(ce)}
|
||||
{phaseCellContent(ce, col.phase, valueMode)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -628,31 +661,57 @@ export function PlanView({
|
||||
// Zellinhalt: Start- UND Zielwert fuer wertbehaftete Elemente, sonst die Kennzahl.
|
||||
const START_END_CATEGORIES: ElementCategory[] = [...VALUE_CATEGORIES, "INCOME", "EXPENSE"];
|
||||
|
||||
function phaseCellContent(ce: ReturnType<PhaseComputed["elements"]["find"]> | undefined): React.ReactNode {
|
||||
type ValueMode = "nominal" | "both" | "real";
|
||||
const VALUE_MODE_KEY = "fpt-value-mode";
|
||||
|
||||
// Realwert = nominal / kumulierte Inflation (Kaufkraft zum Planbeginn).
|
||||
function realOf(nominal: number, deflator: number): number {
|
||||
return Math.round(nominal / (deflator || 1));
|
||||
}
|
||||
function valStr(nominal: number, deflator: number, mode: ValueMode): string {
|
||||
if (mode === "real") return formatChf(realOf(nominal, deflator));
|
||||
if (mode === "both") return `${formatChf(nominal)} (${formatChf(realOf(nominal, deflator))})`;
|
||||
return formatChf(nominal);
|
||||
}
|
||||
|
||||
function phaseCellContent(
|
||||
ce: ReturnType<PhaseComputed["elements"]["find"]> | undefined,
|
||||
phase: PhaseComputed,
|
||||
mode: ValueMode
|
||||
): React.ReactNode {
|
||||
if (!ce) return "–";
|
||||
if (ce.note) return ce.note;
|
||||
const dS = phase.cumulativeInflationStart;
|
||||
const dE = phase.cumulativeInflationEnd;
|
||||
if (START_END_CATEGORIES.includes(ce.category) && (ce.startValue !== 0 || ce.endValue !== 0)) {
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
{formatChf(ce.startValue)} <span className="text-faint">→</span> {formatChf(ce.endValue)}
|
||||
{valStr(ce.startValue, dS, mode)} <span className="text-faint">→</span> {valStr(ce.endValue, dE, mode)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((ce.category === "AHV" || ce.category === "PENSION_FUND") && ce.startValue !== 0) {
|
||||
return `Rente ${valStr(ce.startValue, dS, mode)}`;
|
||||
}
|
||||
return ce.summary || "–";
|
||||
}
|
||||
|
||||
function PhaseHeader({
|
||||
phase,
|
||||
personLabel,
|
||||
mode,
|
||||
onClick,
|
||||
active,
|
||||
}: {
|
||||
phase: PhaseComputed;
|
||||
personLabel: (role: string) => string;
|
||||
mode: ValueMode;
|
||||
onClick: () => void;
|
||||
active: boolean;
|
||||
}) {
|
||||
const quotaLabel = phase.isConsumption ? "Verzehr" : "Quote";
|
||||
const dS = phase.cumulativeInflationStart;
|
||||
const dE = phase.cumulativeInflationEnd;
|
||||
return (
|
||||
<th
|
||||
onClick={onClick}
|
||||
@@ -680,18 +739,17 @@ function PhaseHeader({
|
||||
{personLabel(p.role)} {p.startAge} → {p.endAge}
|
||||
</div>
|
||||
))}
|
||||
<div>Einkommen {formatChf(phase.incomeStart)} → {formatChf(phase.incomeEnd)}</div>
|
||||
<div>Ausgaben {formatChf(phase.expenseStart)} → {formatChf(phase.expenseEnd)}</div>
|
||||
<div>Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dE, mode)}</div>
|
||||
<div>Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dE, mode)}</div>
|
||||
<div>
|
||||
{quotaLabel} {formatChf(phase.quotaStart)} → {formatChf(phase.quotaEnd)}
|
||||
{quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dE, mode)}
|
||||
</div>
|
||||
<div className={phase.cashNegative ? "text-danger font-medium" : ""}>
|
||||
Cash {formatChf(phase.cashStart)} → {formatChf(phase.cashEnd)}
|
||||
Cash {valStr(phase.cashStart, dS, mode)} → {valStr(phase.cashEnd, dE, mode)}
|
||||
</div>
|
||||
<div>
|
||||
Vermoegen {formatChf(phase.startWealthNominal)} → {formatChf(phase.endWealthNominal)}
|
||||
Vermoegen {valStr(phase.startWealthNominal, dS, mode)} → {valStr(phase.endWealthNominal, dE, mode)}
|
||||
</div>
|
||||
<div className="text-faint">real {formatChf(phase.endWealthReal)}</div>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user