Phasenkopf-Ueberarbeitung: zweizeilige Werte, Verfuegbares Kapital, Verteil-Werkzeuge
Deploy App / deploy (push) Successful in 1m5s
Deploy App / deploy (push) Successful in 1m5s
Rein an der Oberflaeche und als neue Bearbeitungswerkzeuge -- keine Aenderung
an Berechnung, Datenmodell oder API. Beide Verteil-Dialoge schreiben nur
bestehende Felder ueber bestehende Endpunkte.
1) Zweizeilige Wertdarstellung im Modus "Beide": Realwert in Klammern in
eigener Zeile UNTER dem nominalen Wert (Kopf + Matrix-Zellen), Pfeil auf
beiden Zeilen. Dadurch schmalere Spalten und jede Kennzahl umbruchfrei.
2) "Sparquote" / "Verzehrquote" statt "Quote" / "Verzehr".
3) Neuer Kopf-Block "Verfuegbares Kapital" (ab Phase 2, nur wenn > 0):
Topf, davon verteilt, Rest auf Cash -- vollstaendig aus der Cash-Bruecke
abgeleitet (capitalPot).
4) Zwei Verteil-Popups mit Live-Vorschau (erneutes computePlan im Browser):
- "Kapital verteilen": Zusatzeinlage (PK/3a/Vermoegen, Phasenwert) +
Sonderamortisation/Sofort-Tilgung (Uebergangswert der Vorphase);
Rest bleibt automatisch auf Cash, Ueberverteilung wird als Luecke gemeldet
- "Sparquote/Bezug verteilen": jaehrliche Raten; zeigt Quote erstes Jahr,
letztes Jahr UND absolut ueber die Phase; warnt, wenn die Quote sinkt
(flache Rate wuerde spaeter Cash-Loch reissen). PK bewusst ausgeschlossen
(Beitrag aus Bruttolohn, belastet Cash nicht).
Neues reines Modul distribution.ts (capitalPot, quotaSummary, applyPatches).
8 Tests (103 -> 111) -- u.a. residual-Kontrolle gegen die Cash-Bruecke und
Nachweis, dass die Quote ueber die Phase sinkt.
SPEZIFIKATION auf 0.14: neue Kapitel 3.6.9, 3.6.10, 9.25; 3.6.1 und 3.6.3
ueberarbeitet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// Verteilung von verfügbarem Kapital und laufender Spar-/Verzehrquote.
|
||||
//
|
||||
// Reine Orchestrierung: Beide Werkzeuge schreiben ausschliesslich Felder, die es längst gibt
|
||||
// (additionalInvestment, extraAmortization, immediateRepayment, annualContribution,
|
||||
// annualWithdrawal, amortization, annualRepayment). An der Berechnung ändert sich nichts --
|
||||
// die Vorschau entsteht, indem der Plan mit den Entwurfswerten kopiert und erneut durch
|
||||
// computePlan geschickt wird. Dadurch ist die angezeigte Wirkung per Konstruktion exakt die
|
||||
// spätere, inklusive aller Kappungen (z. B. Bezugsrate am Bestand, Amortisation an der
|
||||
// Restschuld).
|
||||
|
||||
import type { PhaseComputed } from "@/lib/calculations";
|
||||
import type { PhaseData, TransitionData } from "@/lib/elements";
|
||||
import type { PlanInput } from "@/lib/types";
|
||||
|
||||
// --- Verfügbares Kapital ------------------------------------------------------------------
|
||||
//
|
||||
// Der "Topf" ist das Kapital, das beim Übergang IN diese Phase zur Verfügung steht -- also
|
||||
// bevor etwas davon investiert oder in Schulden gesteckt wurde. Alle Bestandteile stammen aus
|
||||
// der Cash-Brücke (seit 0.11), es braucht keine zusätzliche Berechnung:
|
||||
//
|
||||
// Topf = Cash-Ende der Vorphase + Kapitalzufluss + einmaliger Zufluss − einmalige Kosten
|
||||
// = cashStart + Investitionen + Sofort-Tilgungen
|
||||
//
|
||||
// Die zweite Form ist die, die hier verwendet wird: Sie zerlegt den Topf direkt in
|
||||
// "schon verteilt" und "Rest auf Cash".
|
||||
export interface CapitalPot {
|
||||
total: number; // gesamter verteilbarer Betrag
|
||||
fromPreviousCash: number; // Cash-Endbestand der Vorphase
|
||||
capitalInflow: number; // Verkäufe, PK-/3a-Bezüge
|
||||
oneOffInflow: number; // Erbschaft o. ä. (netto nach Steuer)
|
||||
oneOffOutflow: number; // einmalige Kosten (mindern den Topf)
|
||||
allocatedInvestments: number; // Zusatzeinlagen in PK/3a/Vermögen
|
||||
allocatedRepayments: number; // Sonderamortisation + Sofort-Tilgung
|
||||
rest: number; // bleibt auf dem Cash-Konto (= cashStart)
|
||||
}
|
||||
|
||||
export function capitalPot(phase: PhaseComputed): CapitalPot {
|
||||
const c = phase.cashBridge;
|
||||
return {
|
||||
total: c.cashStart + c.investments + c.immediateRepay,
|
||||
fromPreviousCash: c.openingCash,
|
||||
capitalInflow: c.capitalInflow,
|
||||
oneOffInflow: c.oneOffInflow,
|
||||
oneOffOutflow: c.oneOffOutflow,
|
||||
allocatedInvestments: c.investments,
|
||||
allocatedRepayments: c.immediateRepay,
|
||||
rest: c.cashStart,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Spar-/Verzehrquote -------------------------------------------------------------------
|
||||
//
|
||||
// Die Quote ist KEIN fester Betrag: Einkommen wächst mit der Lohnerhöhung, Ausgaben mit der
|
||||
// Inflation. Deshalb werden drei Grössen ausgewiesen -- erstes Jahr, letztes Jahr und die
|
||||
// Summe über die ganze Phase ("absolute Quote"). Die Summe ist die Grösse, gegen die sich
|
||||
// eine flache Jahresrate sinnvoll verteilen lässt.
|
||||
//
|
||||
// Hinweis zur Einheit: `total` summiert nominale Franken verschiedener Jahre -- exakt so, wie
|
||||
// das Cash-Konto im Modell funktioniert. Für die Frage "wie viel Cash steht über die Phase
|
||||
// zur Verfügung" ist das die richtige Grösse, als Kaufkraft-Aussage taugt sie nicht.
|
||||
export interface QuotaSummary {
|
||||
start: number; // Quote im ersten Phasenjahr
|
||||
end: number; // Quote im letzten Phasenjahr
|
||||
total: number; // Summe über alle Phasenjahre
|
||||
allocatedOut: number; // Sparbeiträge + Amortisationen + Tilgungen, über die Phase summiert
|
||||
allocatedIn: number; // Bezugsraten, über die Phase summiert
|
||||
netToCash: number; // was unter dem Strich aufs Cash geht (= cashEnd − cashStart)
|
||||
isConsumption: boolean;
|
||||
}
|
||||
|
||||
export function quotaSummary(phase: PhaseComputed): QuotaSummary {
|
||||
const c = phase.cashBridge;
|
||||
const allocatedOut = c.savingRates + c.debtRates;
|
||||
return {
|
||||
start: phase.quotaStart,
|
||||
end: phase.quotaEnd,
|
||||
total: c.quotaTotal,
|
||||
allocatedOut,
|
||||
allocatedIn: c.withdrawals,
|
||||
netToCash: c.quotaTotal - allocatedOut + c.withdrawals,
|
||||
isConsumption: c.quotaTotal < 0,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Entwurfswerte auf einen Plan anwenden (für die Live-Vorschau) ------------------------
|
||||
// Beide Funktionen sind rein und lassen den Ausgangsplan unberührt.
|
||||
|
||||
export interface PhasePatch {
|
||||
elementId: string;
|
||||
field: keyof PhaseData;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function applyPhasePatches(plan: PlanInput, phaseId: string, patches: PhasePatch[]): PlanInput {
|
||||
if (patches.length === 0) return plan;
|
||||
const byElement = new Map<string, PhasePatch[]>();
|
||||
for (const p of patches) {
|
||||
const list = byElement.get(p.elementId) ?? [];
|
||||
list.push(p);
|
||||
byElement.set(p.elementId, list);
|
||||
}
|
||||
return {
|
||||
...plan,
|
||||
elements: plan.elements.map((e) => {
|
||||
const list = byElement.get(e.id);
|
||||
if (!list) return e;
|
||||
const merged: PhaseData = { ...(e.phaseValues[phaseId] ?? {}) };
|
||||
for (const p of list) (merged as Record<string, number>)[p.field as string] = p.value;
|
||||
return { ...e, phaseValues: { ...e.phaseValues, [phaseId]: merged } };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export interface TransitionPatch {
|
||||
elementId: string;
|
||||
field: keyof TransitionData;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function applyTransitionPatches(
|
||||
plan: PlanInput,
|
||||
fromPhaseId: string,
|
||||
patches: TransitionPatch[]
|
||||
): PlanInput {
|
||||
if (patches.length === 0) return plan;
|
||||
const byElement = new Map<string, TransitionPatch[]>();
|
||||
for (const p of patches) {
|
||||
const list = byElement.get(p.elementId) ?? [];
|
||||
list.push(p);
|
||||
byElement.set(p.elementId, list);
|
||||
}
|
||||
return {
|
||||
...plan,
|
||||
elements: plan.elements.map((e) => {
|
||||
const list = byElement.get(e.id);
|
||||
if (!list) return e;
|
||||
// Bestehende Entscheide (decision, salePrice, payoutMode …) bleiben erhalten -- hier
|
||||
// werden nur die Betragsfelder überschrieben.
|
||||
const merged: TransitionData = { ...(e.transitionValues[fromPhaseId] ?? {}) };
|
||||
for (const p of list) (merged as Record<string, number>)[p.field as string] = p.value;
|
||||
return { ...e, transitionValues: { ...e.transitionValues, [fromPhaseId]: merged } };
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user