diff --git a/src/components/ElementDetail.tsx b/src/components/ElementDetail.tsx index d010dff..4147f1e 100644 --- a/src/components/ElementDetail.tsx +++ b/src/components/ElementDetail.tsx @@ -455,7 +455,19 @@ export function ElementDetail({ element, context, phaseData, transitionData, onS if (isTransition) { await api.put(`/api/elements/${element.id}/transition/${context.phaseId}`, td); } else { - await api.put(`/api/elements/${element.id}/phase/${context.phaseId}`, pd); + const payload: PhaseData = { ...pd }; + // Einkommen/Ausgaben ab Phase 2: entspricht der Basiswert dem fortgeschriebenen Wert + // der Vorphase, KEINEN Override speichern -> Wert bleibt live vererbt (Aenderungen in + // frueheren Phasen wirken sich weiter aus). Nur ein bewusst abweichender Wert wird fix. + if ( + (element.category === "INCOME" || element.category === "EXPENSE") && + context.carried && + typeof payload.amount === "number" && + payload.amount === Math.round(context.derivedStart) + ) { + delete payload.amount; + } + await api.put(`/api/elements/${element.id}/phase/${context.phaseId}`, payload); } onSaved(); } catch (e) { diff --git a/src/components/PlanView.tsx b/src/components/PlanView.tsx index dbe4341..3cde570 100644 --- a/src/components/PlanView.tsx +++ b/src/components/PlanView.tsx @@ -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("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(() => { const cols: Column[] = []; @@ -239,6 +250,25 @@ export function PlanView({ return (
+
+ Anzeige +
+ {(["nominal", "both", "real"] as ValueMode[]).map((m) => ( + + ))} +
+ real = kaufkraftbereinigt (Planbeginn) +
+ {/* 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"}`} > - {formatChf(col.phase.cashStart)} {formatChf(col.phase.cashEnd)} + {valStr(col.phase.cashStart, col.phase.cumulativeInflationStart, valueMode)}{" "} + {" "} + {valStr(col.phase.cashEnd, col.phase.cumulativeInflationEnd, valueMode)} ) : ( @@ -402,7 +435,7 @@ export function PlanView({ ce?.locked ? "text-faint" : "text-fg" }`} > - {phaseCellContent(ce)} + {phaseCellContent(ce, col.phase, valueMode)} ); } @@ -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 | 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 | 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 ( - {formatChf(ce.startValue)} {formatChf(ce.endValue)} + {valStr(ce.startValue, dS, mode)} {valStr(ce.endValue, dE, mode)} ); } + 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 ( ))} -
Einkommen {formatChf(phase.incomeStart)} → {formatChf(phase.incomeEnd)}
-
Ausgaben {formatChf(phase.expenseStart)} → {formatChf(phase.expenseEnd)}
+
Einkommen {valStr(phase.incomeStart, dS, mode)} → {valStr(phase.incomeEnd, dE, mode)}
+
Ausgaben {valStr(phase.expenseStart, dS, mode)} → {valStr(phase.expenseEnd, dE, mode)}
- {quotaLabel} {formatChf(phase.quotaStart)} → {formatChf(phase.quotaEnd)} + {quotaLabel} {valStr(phase.quotaStart, dS, mode)} → {valStr(phase.quotaEnd, dE, mode)}
- Cash {formatChf(phase.cashStart)} → {formatChf(phase.cashEnd)} + Cash {valStr(phase.cashStart, dS, mode)} → {valStr(phase.cashEnd, dE, mode)}
- Vermoegen {formatChf(phase.startWealthNominal)} → {formatChf(phase.endWealthNominal)} + Vermoegen {valStr(phase.startWealthNominal, dS, mode)} → {valStr(phase.endWealthNominal, dE, mode)}
-
real {formatChf(phase.endWealthReal)}
); diff --git a/src/lib/calculations.ts b/src/lib/calculations.ts index a07716f..f155cf9 100644 --- a/src/lib/calculations.ts +++ b/src/lib/calculations.ts @@ -62,7 +62,8 @@ export interface PhaseComputed { elements: ElementPhaseComputed[]; startWealthNominal: number; // inkl. Cash endWealthNominal: number; // inkl. Cash - cumulativeInflationEnd: number; + cumulativeInflationStart: number; // Kaufkraft-Deflator zu Phasenbeginn (Startwerte) + cumulativeInflationEnd: number; // Kaufkraft-Deflator am Phasenende (Endwerte) endWealthReal: number; } @@ -408,6 +409,7 @@ export function computePlan(plan: PlanInput): PlanComputed { const cashEnd = Math.round(cash); const startWealthNominal = Math.round(wealthStart + cashStart); const endWealthNominal = Math.round(wealthEnd + cashEnd); + const cumulativeInflationStart = cumulativeInflation; cumulativeInflation = cumulativeInflation * Math.pow(1 + phaseInflation / 100, duration); result.push({ @@ -433,6 +435,7 @@ export function computePlan(plan: PlanInput): PlanComputed { elements: orderedElements.map((e) => ecById.get(e.id)!), startWealthNominal, endWealthNominal, + cumulativeInflationStart, cumulativeInflationEnd: cumulativeInflation, endWealthReal: endWealthNominal / cumulativeInflation, });