Einmalige Sonderein-/ausgaben am Cash-Uebergang (Roadmap Nr. 1)
Deploy App / deploy (push) Successful in 1m51s
Deploy App / deploy (push) Successful in 1m51s
Der Cash-Uebergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 uebernehmen / einmaliger Zufluss / einmalige Kosten / beides. Die Betraege gehen direkt aufs Cash-Konto und bleiben aus der Spar-/Verzehrquote heraus. - Zufluss NOMINAL erfasst, real angezeigt (wie Einkommen), optionaler Steuersatz (Default 0 %). Kosten REAL erfasst, nominal angezeigt (wie Ausgaben). Umrechnung ueber den Bestands-Deflator an der Phasengrenze. - Entscheid startet unbeantwortet und zaehlt im "offen"-Badge mit; eine neue Phase erzeugt damit automatisch einen offenen Cash-Entscheid am neuen Uebergang. - Eigene Kopf-Kennzahlen statt Vermischung mit Kapitalzufluss/-investitionen: eine Erbschaft ist kein Verkaufserloes, ein Poolbau keine Investition. - Cash ist kein FinancialElement -> der Entscheid haengt als JSON an der Von-Phase (neue Spalte Phase.cashTransition + Migration). Szenario-Kopie nimmt ihn mit. Fuenf Regressionstests ergaenzt (13 -> 18). Spezifikation auf v0.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { FieldLabel, MoneyField, NumberField, SelectField } from "@/components/FormField";
|
||||
import { FieldLabel, MoneyField, NumberField, SelectField, TextField } from "@/components/FormField";
|
||||
import { formatChf } from "@/lib/format";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { CATEGORY_LABELS, num } from "@/lib/elements";
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
DEFAULT_PROPERTY_GAINS_TAX_RATE,
|
||||
PILLAR_3A_MAX_ANNUAL,
|
||||
} from "@/lib/constants";
|
||||
import type { ElementCategory, PhaseData, TransitionData } from "@/lib/elements";
|
||||
import type {
|
||||
CashTransitionData,
|
||||
CashTransitionMode,
|
||||
ElementCategory,
|
||||
PhaseData,
|
||||
TransitionData,
|
||||
} from "@/lib/elements";
|
||||
|
||||
export interface CellContext {
|
||||
kind: "phase" | "transition";
|
||||
@@ -121,6 +127,147 @@ export function withTransitionDefaults(category: ElementCategory, isRetirement:
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Cash-Uebergang: einmalige Sonderein-/ausgaben ---
|
||||
|
||||
export function withCashTransitionDefaults(ct: CashTransitionData): CashTransitionData {
|
||||
return ct.mode === undefined ? { ...ct, mode: "NONE" } : { ...ct };
|
||||
}
|
||||
|
||||
export function isCashTransitionAnswered(ct: CashTransitionData): boolean {
|
||||
return ct.mode !== undefined;
|
||||
}
|
||||
|
||||
// Kurzfassung fuer die Uebergangszelle der Cash-Zeile.
|
||||
export function cashTransitionSummary(ct: CashTransitionData): string {
|
||||
const mode = ct.mode;
|
||||
if (mode === undefined) return "?";
|
||||
const inn = `+${formatChf(num(ct.inflowAmount))}`;
|
||||
const out = `−${formatChf(num(ct.outflowAmount))}`;
|
||||
switch (mode) {
|
||||
case "INFLOW":
|
||||
return inn;
|
||||
case "OUTFLOW":
|
||||
return out;
|
||||
case "BOTH":
|
||||
return `${inn} / ${out}`;
|
||||
default:
|
||||
return "1:1";
|
||||
}
|
||||
}
|
||||
|
||||
// Eingabefelder fuer den Cash-Entscheid. Erfassungs-Konventionen bewusst wie bei den
|
||||
// laufenden Flows: Zufluss nominal (wie Einkommen), Kosten real (wie Ausgaben).
|
||||
export function CashTransitionFields({
|
||||
ct,
|
||||
setC,
|
||||
deflatorEnd,
|
||||
}: {
|
||||
ct: CashTransitionData;
|
||||
setC: (patch: Partial<CashTransitionData>) => void;
|
||||
deflatorEnd: number; // Bestands-Deflator an der Phasengrenze
|
||||
}) {
|
||||
const mode = ct.mode ?? "NONE";
|
||||
const showIn = mode === "INFLOW" || mode === "BOTH";
|
||||
const showOut = mode === "OUTFLOW" || mode === "BOTH";
|
||||
const d = deflatorEnd || 1;
|
||||
const inflowGross = num(ct.inflowAmount);
|
||||
const taxRate = num(ct.inflowTaxRate, 0);
|
||||
const inflowNet = Math.round(inflowGross * (1 - taxRate / 100));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sm:col-span-2">
|
||||
<SelectField
|
||||
label="Entscheid fuer das Cash-Konto"
|
||||
help="Was passiert beim Uebergang in die naechste Lebensphase mit dem Cash-Bestand?"
|
||||
value={mode}
|
||||
onChange={(v: CashTransitionMode) =>
|
||||
setC(
|
||||
v === "NONE"
|
||||
? { mode: v, inflowAmount: 0, outflowAmount: 0 }
|
||||
: v === "INFLOW"
|
||||
? { mode: v, outflowAmount: 0 }
|
||||
: v === "OUTFLOW"
|
||||
? { mode: v, inflowAmount: 0 }
|
||||
: { mode: v }
|
||||
)
|
||||
}
|
||||
options={[
|
||||
{ value: "NONE", label: "1:1 uebernehmen" },
|
||||
{ value: "INFLOW", label: "Einmaliger Zufluss" },
|
||||
{ value: "OUTFLOW", label: "Einmalige Kosten" },
|
||||
{ value: "BOTH", label: "Zufluss und Kosten" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showIn && (
|
||||
<>
|
||||
<div className="sm:col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
<strong className="text-fg">Einmaliger Zufluss</strong> (z. B. Erbschaft). Wird NOMINAL erfasst –
|
||||
der Betrag, der zu diesem Zeitpunkt tatsaechlich aufs Konto kommt.
|
||||
</div>
|
||||
<TextField
|
||||
label="Bezeichnung"
|
||||
value={ct.inflowLabel ?? ""}
|
||||
placeholder="z. B. Erbschaft"
|
||||
onChange={(v) => setC({ inflowLabel: v })}
|
||||
/>
|
||||
<MoneyField
|
||||
label="Betrag NOMINAL (CHF)"
|
||||
value={inflowGross}
|
||||
onChange={(v) => setC({ inflowAmount: v })}
|
||||
/>
|
||||
<NumberField
|
||||
label="Steuer (%)"
|
||||
help="Z. B. Erbschafts-/Schenkungssteuer. Direkte Nachkommen sind in den meisten Kantonen befreit – Default 0 %."
|
||||
step={0.5}
|
||||
value={taxRate}
|
||||
onChange={(v) => setC({ inflowTaxRate: v })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="≈ real (heutige Kaufkraft)"
|
||||
value={Math.round(inflowNet / d)}
|
||||
help="Netto-Zufluss nach Steuer, zurueckgerechnet auf die Kaufkraft bei Planbeginn. Nur zur Info."
|
||||
/>
|
||||
{taxRate > 0 && (
|
||||
<DerivedField
|
||||
label="Netto aufs Cash (nominal)"
|
||||
value={inflowNet}
|
||||
help="Betrag abzueglich Steuer. Wird automatisch berechnet."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showOut && (
|
||||
<>
|
||||
<div className="sm:col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||
<strong className="text-fg">Einmalige Kosten</strong> (z. B. Poolbau). Werden REAL erfasst –
|
||||
in heutiger Kaufkraft. Die Inflation rechnet daraus automatisch den nominalen Betrag.
|
||||
</div>
|
||||
<TextField
|
||||
label="Bezeichnung"
|
||||
value={ct.outflowLabel ?? ""}
|
||||
placeholder="z. B. Poolbau"
|
||||
onChange={(v) => setC({ outflowLabel: v })}
|
||||
/>
|
||||
<MoneyField
|
||||
label="Betrag REAL (CHF, heutige Kaufkraft)"
|
||||
value={num(ct.outflowAmount)}
|
||||
onChange={(v) => setC({ outflowAmount: v })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="≈ nominal (zum Zeitpunkt)"
|
||||
value={Math.round(num(ct.outflowAmount) * d)}
|
||||
help="Der Betrag, der zu diesem Zeitpunkt tatsaechlich faellig ist. Nur zur Info."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// "Beantwortet" = ein konkreter Entscheid liegt vor (kein offenes Fragezeichen).
|
||||
export function isTransitionAnswered(category: ElementCategory, isRetirement: boolean, td: TransitionData): boolean {
|
||||
switch (category) {
|
||||
|
||||
Reference in New Issue
Block a user