Files
FPT/src/lib/distribution.ts
T
admGitAICDS 9252f7188d
Deploy App / deploy (push) Successful in 1m5s
Phasenkopf-Ueberarbeitung: zweizeilige Werte, Verfuegbares Kapital, Verteil-Werkzeuge
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>
2026-07-19 12:30:36 +02:00

145 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 } };
}),
};
}