V3: 3 Farbschemata, Grundprofil auf Plan-Ebene, Erstellungs-Popups, integer-Zahlenfelder mit Beschleunigungs-Spinner, Carry-Forward des Zielwerts, gefuehrter Uebergang
Deploy App / deploy (push) Successful in 1m51s
Deploy App / deploy (push) Successful in 1m51s
- Theming: semantische CSS-Tokens + 3 waehlbare Schemata (Hell/Dunkel/Warm/Sunset), Umschalter im Profil-Menue, FOUC-frei via Inline-Script, localStorage; Klassen-Sweep aller Komponenten, Recharts aus Tokens - Datenmodell: Household entfaellt; Plan traegt Haushaltsform/Personen/Inflation selbst (Person -> planId, Plan -> userId); destruktive Migration (TRUNCATE); Onboarding/HouseholdSettings entfernt; Plan-Erstellung & -Einstellungen mit Profilfeldern - Popups: Element-Erstellung mit Inline-Feldern (geteilte ElementPhaseFields/ElementTransitionFields), Phase- und Plan-Popups mit Direkteingabe - Zahlenfelder: 1'000er-Runden entfernt (floorToThousand/roundToHundred weg), integer MoneyInput mit beschleunigendem Press-and-Hold-Spinner, 0-Bug-Fix, harte Live-Caps - Quote: Amortisation + Tilgung neu quotenwirksam; Restquote sichtbar (sinkt beim Verteilen); Invest-Deckel = verfuegbares Kapital + fortgeschriebener Zielwert - Carry-Forward: Startwert der Folgephase = Zielwert der Vorphase minus Uebergangs-Bezug (live abgeleitet); optionale Zusatzinvestition aus verfuegbarem Kapital - Matrix: Zelle zeigt Start -> Ziel; Uebergangs-Spaltenkopf mit "n offen"-Badge + gefuehrtem Pruef-Panel Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+75
-74
@@ -3,10 +3,9 @@ import {
|
||||
AHV_FULL_CONTRIBUTION_YEARS,
|
||||
AHV_MAX_ANNUAL_SINGLE,
|
||||
} from "@/lib/constants";
|
||||
import { floorToThousand } from "@/lib/format";
|
||||
import { num } from "@/lib/elements";
|
||||
import type { ElementCategory } from "@/lib/elements";
|
||||
import type { HouseholdInput, PersonRole, PlanInput } from "@/lib/types";
|
||||
import type { PersonRole, PlanInput } from "@/lib/types";
|
||||
|
||||
export type PhaseType = "ERWERB" | "PENSION" | "MIXED";
|
||||
export type ElementStatus = "ACTIVE" | "SOLD" | "SETTLED";
|
||||
@@ -28,11 +27,12 @@ export interface ElementPhaseComputed {
|
||||
ownerRole: string | null;
|
||||
status: ElementStatus;
|
||||
locked: boolean; // verkauft/getilgt -> in dieser Phase nicht mehr editierbar
|
||||
carried: boolean; // Startwert wird aus der Vorphase fortgeschrieben (Phase >= 2)
|
||||
startValue: number; // Netto-Wert zu Phasenbeginn (Aktiven +, Schulden -)
|
||||
endValue: number; // Netto-Wert am Phasenende
|
||||
incomeContribution: number; // Beitrag zum Phasen-Einkommen
|
||||
expenseContribution: number; // Beitrag zu den Phasen-Ausgaben
|
||||
quotaUse: number; // Betrag, der Spar-/Verzehrquote verbraucht (3a/Sonstiges Vermoegen)
|
||||
quotaUse: number; // Betrag, der Spar-/Verzehrquote verbraucht (3a/Vermoegen/Amort./Tilgung)
|
||||
capitalUse: number; // verbrauchtes verfuegbares Startkapital (Aufstockung/Neuinvestition)
|
||||
summary: string; // Kennzahl fuer die eingeklappte Zelle
|
||||
note: string | null; // z. B. "Verkauft", "Getilgt", "Vollstaendig bezogen"
|
||||
@@ -51,9 +51,11 @@ export interface PhaseComputed {
|
||||
quota: number; // Einkommen - Ausgaben (Sparquote wenn >=0, Verzehrquote wenn <0)
|
||||
isConsumption: boolean;
|
||||
quotaAllocated: number; // verteilte Sparbeitraege bzw. gedeckte Verzehr-Bezuege
|
||||
quotaRemaining: number; // |quota| - quotaAllocated (offener Rest, kann negativ = ueberzogen)
|
||||
quotaComplete: boolean;
|
||||
availableCapital: number | null; // null in der ersten Phase
|
||||
availableCapitalUsed: number;
|
||||
availableCapitalRemaining: number; // 0 in der ersten Phase
|
||||
availableCapitalComplete: boolean;
|
||||
incomplete: boolean; // roter Status (Quote/Kapital nicht vollstaendig verteilt)
|
||||
elements: ElementPhaseComputed[];
|
||||
@@ -68,28 +70,16 @@ export interface PlanComputed {
|
||||
nachlass: number;
|
||||
}
|
||||
|
||||
// Loest das effektive Pensionsalter einer Person auf (Plan-Override vor Profil-Default).
|
||||
export function resolveRetirementAge(
|
||||
role: PersonRole,
|
||||
plan: { retirementAgeA: number | null; retirementAgeB: number | null },
|
||||
profileDefault: number
|
||||
): number {
|
||||
const override = role === "PERSON_A" ? plan.retirementAgeA : plan.retirementAgeB;
|
||||
return override ?? profileDefault;
|
||||
}
|
||||
|
||||
// Maximale Dauer einer neuen Phase, die yearsBefore Jahre nach Planbeginn startet:
|
||||
// bis zum naechsten Pensionsereignis einer noch erwerbenden Person (null = unbegrenzt).
|
||||
export function maxPhaseDuration(
|
||||
persons: { role: PersonRole; age: number; retirementAge: number }[],
|
||||
plan: { retirementAgeA: number | null; retirementAgeB: number | null },
|
||||
yearsBefore: number
|
||||
): number | null {
|
||||
const caps: number[] = [];
|
||||
for (const p of persons) {
|
||||
const ra = resolveRetirementAge(p.role, plan, p.retirementAge);
|
||||
const startAge = p.age + yearsBefore;
|
||||
if (startAge < ra) caps.push(ra - startAge);
|
||||
if (startAge < p.retirementAge) caps.push(p.retirementAge - startAge);
|
||||
}
|
||||
return caps.length > 0 ? Math.min(...caps) : null;
|
||||
}
|
||||
@@ -108,23 +98,23 @@ function emptyCarry(): Carry {
|
||||
return { status: "ACTIVE", value: 0, mortgage: 0, owed: 0, pkPensionAnnual: 0, hasCarry: false };
|
||||
}
|
||||
|
||||
// Zinseszins mit jaehrlichem Beitrag; kein Zwischen-Runden mehr (1'000er-Konzept entfernt),
|
||||
// nur das Endresultat wird auf ganze Franken gerundet.
|
||||
function growAsset(startValue: number, expectedReturn: number, annual: number, years: number): number {
|
||||
let v = floorToThousand(startValue);
|
||||
let v = startValue;
|
||||
for (let y = 0; y < years; y++) {
|
||||
v = floorToThousand(v * (1 + expectedReturn / 100) + annual);
|
||||
v = v * (1 + expectedReturn / 100) + annual;
|
||||
}
|
||||
return Math.max(0, v);
|
||||
return Math.max(0, Math.round(v));
|
||||
}
|
||||
|
||||
export function computePlan(plan: PlanInput, household: HouseholdInput): PlanComputed {
|
||||
export function computePlan(plan: PlanInput): PlanComputed {
|
||||
const phases = [...plan.phases].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
||||
const persons = household.persons;
|
||||
const persons = plan.persons;
|
||||
|
||||
// Pensionsalter je Person (aufgeloest).
|
||||
// Pensionsalter je Person (liegt direkt am plan-eigenen Personensatz).
|
||||
const retirementAge = new Map<string, number>();
|
||||
for (const p of persons) {
|
||||
retirementAge.set(p.id, resolveRetirementAge(p.role, plan, p.retirementAge));
|
||||
}
|
||||
for (const p of persons) retirementAge.set(p.id, p.retirementAge);
|
||||
|
||||
// Kumulierte AHV-Ausfalljahre je Person (ueber die Erwerbsphasen aufsummiert).
|
||||
const gapYearsByPerson = new Map<string, number>();
|
||||
@@ -185,14 +175,14 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
if (!owner || workingByPerson.get(owner.id)) continue; // nur pensionierte Personen
|
||||
const gap = gapYearsByPerson.get(owner.id) ?? 0;
|
||||
const factor = Math.max(0, (AHV_FULL_CONTRIBUTION_YEARS - gap) / AHV_FULL_CONTRIBUTION_YEARS);
|
||||
ahvUncapped.set(owner.id, floorToThousand(AHV_MAX_ANNUAL_SINGLE * factor));
|
||||
ahvUncapped.set(owner.id, Math.round(AHV_MAX_ANNUAL_SINGLE * factor));
|
||||
}
|
||||
const ahvFinal = new Map(ahvUncapped);
|
||||
if (household.householdType === "COUPLE" && ahvUncapped.size === 2) {
|
||||
if (plan.householdType === "COUPLE" && ahvUncapped.size === 2) {
|
||||
const sum = [...ahvUncapped.values()].reduce((a, b) => a + b, 0);
|
||||
const cap = AHV_MAX_ANNUAL_SINGLE * AHV_COUPLE_CAP_FACTOR;
|
||||
if (sum > cap && sum > 0) {
|
||||
for (const [pid, v] of ahvUncapped) ahvFinal.set(pid, floorToThousand(v * (cap / sum)));
|
||||
for (const [pid, v] of ahvUncapped) ahvFinal.set(pid, Math.round(v * (cap / sum)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +207,7 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
ownerRole: e.ownerRole,
|
||||
status: carry.status,
|
||||
locked: carry.status !== "ACTIVE",
|
||||
carried: carry.hasCarry,
|
||||
startValue: 0,
|
||||
endValue: 0,
|
||||
incomeContribution: 0,
|
||||
@@ -242,14 +233,14 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
|
||||
switch (e.category) {
|
||||
case "INCOME": {
|
||||
const amount = floorToThousand(num(pd.amount));
|
||||
const amount = Math.round(num(pd.amount));
|
||||
ec.incomeContribution = amount;
|
||||
incomeTotal += amount;
|
||||
ec.summary = fmt(amount);
|
||||
break;
|
||||
}
|
||||
case "EXPENSE": {
|
||||
const amount = floorToThousand(num(pd.amount));
|
||||
const amount = Math.round(num(pd.amount));
|
||||
ec.expenseContribution = amount;
|
||||
expenseTotal += amount;
|
||||
ec.summary = fmt(amount);
|
||||
@@ -278,13 +269,15 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
ec.note = "Vollstaendig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
} else {
|
||||
const start = floorToThousand(num(pd.currentValue));
|
||||
const contribution = floorToThousand(num(pd.annualContribution));
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
const start = base + topUp;
|
||||
const contribution = Math.round(num(pd.annualContribution));
|
||||
const r = num(pd.expectedReturn);
|
||||
ec.startValue = start;
|
||||
ec.endValue = growAsset(start, r, contribution, phase.durationYears);
|
||||
ec.capitalUse = Math.max(0, start - carry.value);
|
||||
capitalUsed += ec.capitalUse;
|
||||
ec.capitalUse = topUp;
|
||||
capitalUsed += topUp;
|
||||
// PK-Beitraege zaehlen NICHT gegen die Sparquote (in Ausgaben enthalten).
|
||||
ec.summary = fmt(ec.endValue);
|
||||
}
|
||||
@@ -295,13 +288,15 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
ec.note = "Vollstaendig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
} else {
|
||||
const start = floorToThousand(num(pd.currentValue));
|
||||
const contribution = roundToHundred(num(pd.annualContribution));
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
const start = base + topUp;
|
||||
const contribution = Math.round(num(pd.annualContribution));
|
||||
const r = num(pd.expectedReturn);
|
||||
ec.startValue = start;
|
||||
ec.endValue = growAsset(start, r, contribution, phase.durationYears);
|
||||
ec.capitalUse = Math.max(0, start - carry.value);
|
||||
capitalUsed += ec.capitalUse;
|
||||
ec.capitalUse = topUp;
|
||||
capitalUsed += topUp;
|
||||
ec.quotaUse = contribution; // zaehlt gegen die Sparquote
|
||||
quotaAllocated += contribution;
|
||||
ec.summary = fmt(ec.endValue);
|
||||
@@ -309,15 +304,16 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
break;
|
||||
}
|
||||
case "OTHER_ASSET": {
|
||||
const start = floorToThousand(num(pd.startValue));
|
||||
const contribution = floorToThousand(num(pd.annualContribution));
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.startValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
const start = base + topUp;
|
||||
const contribution = Math.round(num(pd.annualContribution));
|
||||
const r = num(pd.expectedReturn);
|
||||
ec.startValue = start;
|
||||
ec.capitalUse = Math.max(0, start - carry.value);
|
||||
capitalUsed += ec.capitalUse;
|
||||
// In Erwerbsphasen (Sparen) wird eingezahlt, in Verzehrphasen bezogen -- das
|
||||
// Vorzeichen ergibt sich aus der Phasenquote (siehe unten). Hier immer als
|
||||
// Beitrag verbucht; die Verzehr-Logik nutzt denselben Betrag als Bezug.
|
||||
ec.capitalUse = topUp;
|
||||
capitalUsed += topUp;
|
||||
// In Erwerbsphasen Sparbeitrag, in Verzehrphasen Bezugsrate -- beides zaehlt gegen
|
||||
// die Quote (Vorzeichen ergibt sich aus der Phasenquote).
|
||||
ec.quotaUse = contribution;
|
||||
quotaAllocated += contribution;
|
||||
ec.endValue = growAsset(start, r, contribution, phase.durationYears);
|
||||
@@ -325,9 +321,9 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
break;
|
||||
}
|
||||
case "REAL_ESTATE": {
|
||||
const purchase = floorToThousand(num(pd.purchasePrice));
|
||||
const mortgageStart = carry.hasCarry ? carry.mortgage : floorToThousand(num(pd.mortgage));
|
||||
const amort = floorToThousand(num(pd.amortization));
|
||||
const purchase = Math.round(num(pd.purchasePrice));
|
||||
const mortgageStart = carry.hasCarry ? carry.mortgage : Math.round(num(pd.mortgage));
|
||||
const amort = Math.round(num(pd.amortization));
|
||||
const mortgageEnd = Math.max(0, mortgageStart - amort * phase.durationYears);
|
||||
ec.startValue = purchase - mortgageStart;
|
||||
ec.endValue = purchase - mortgageEnd;
|
||||
@@ -335,17 +331,23 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
ec.capitalUse = Math.max(0, purchase - mortgageStart); // Eigenkapital bei Neukauf
|
||||
capitalUsed += ec.capitalUse;
|
||||
}
|
||||
// Amortisation ist quotenwirksam (jaehrlicher Budgetbetrag).
|
||||
ec.quotaUse = amort;
|
||||
quotaAllocated += amort;
|
||||
carry.mortgage = mortgageEnd; // fuer Uebergang
|
||||
ec.summary = fmt(ec.endValue);
|
||||
break;
|
||||
}
|
||||
case "OTHER_DEBT": {
|
||||
const owedStart = carry.hasCarry ? carry.owed : floorToThousand(num(pd.startValue));
|
||||
const repay = floorToThousand(num(pd.annualRepayment));
|
||||
const owedStart = carry.hasCarry ? carry.owed : Math.round(num(pd.startValue));
|
||||
const repay = Math.round(num(pd.annualRepayment));
|
||||
const owedEnd = Math.max(0, owedStart - repay * phase.durationYears);
|
||||
ec.startValue = -owedStart;
|
||||
ec.endValue = -owedEnd;
|
||||
carry.owed = owedEnd;
|
||||
// Tilgung ist quotenwirksam (jaehrlicher Budgetbetrag).
|
||||
ec.quotaUse = repay;
|
||||
quotaAllocated += repay;
|
||||
ec.summary = fmt(ec.endValue);
|
||||
if (owedEnd === 0) ec.note = "Wird getilgt";
|
||||
break;
|
||||
@@ -361,16 +363,18 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
// Sparphase: alles verteilt, wenn quotaAllocated == quota. Verzehrphase: gedeckt,
|
||||
// wenn Bezuege (quotaAllocated) den Fehlbetrag decken.
|
||||
const quotaTarget = Math.abs(quota);
|
||||
const quotaComplete = Math.abs(quotaTarget - quotaAllocated) < 1;
|
||||
const quotaRemaining = quotaTarget - quotaAllocated;
|
||||
const quotaComplete = Math.abs(quotaRemaining) < 1;
|
||||
|
||||
const availableCapital = incomingCapital;
|
||||
const availableCapitalUsed = capitalUsed;
|
||||
const availableCapitalRemaining = availableCapital === null ? 0 : availableCapital - availableCapitalUsed;
|
||||
const availableCapitalComplete =
|
||||
availableCapital === null || Math.abs(availableCapital - availableCapitalUsed) < 1;
|
||||
availableCapital === null || Math.abs(availableCapitalRemaining) < 1;
|
||||
|
||||
const incomplete = !quotaComplete || !availableCapitalComplete;
|
||||
|
||||
const inflationRate = phase.inflationRate ?? household.inflationRateDefault;
|
||||
const inflationRate = phase.inflationRate ?? plan.inflationRateDefault;
|
||||
cumulativeInflation = cumulativeInflation * (1 + inflationRate / 100);
|
||||
|
||||
const startWealthNominal = elementsComputed.reduce((s, ec) => s + ec.startValue, 0);
|
||||
@@ -389,9 +393,11 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
quota,
|
||||
isConsumption,
|
||||
quotaAllocated,
|
||||
quotaRemaining,
|
||||
quotaComplete,
|
||||
availableCapital,
|
||||
availableCapitalUsed,
|
||||
availableCapitalRemaining,
|
||||
availableCapitalComplete,
|
||||
incomplete,
|
||||
elements: elementsComputed,
|
||||
@@ -409,7 +415,7 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
const td = e.transitionValues[phase.id] ?? {};
|
||||
const owner = e.ownerRole && e.ownerRole !== "HOUSEHOLD" ? personByRole(persons, e.ownerRole) : null;
|
||||
const ownerRetiresNext =
|
||||
!!owner && !!nextPhase && workingByPerson.get(owner.id) === true && retiresInPhase(owner.id, nextPhase, persons, retirementAge, yearsBefore + phase.durationYears);
|
||||
!!owner && !!nextPhase && workingByPerson.get(owner.id) === true && retiresInPhase(owner.id, persons, retirementAge, yearsBefore + phase.durationYears);
|
||||
|
||||
if (carry.status !== "ACTIVE") {
|
||||
carry.hasCarry = true;
|
||||
@@ -422,22 +428,22 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
const value = ec.endValue;
|
||||
const mode = td.payoutMode ?? "PENSION";
|
||||
if (mode === "CAPITAL") {
|
||||
const net = floorToThousand(value * (1 - num(td.capitalTaxRate) / 100));
|
||||
const net = Math.round(value * (1 - num(td.capitalTaxRate) / 100));
|
||||
outgoing += net;
|
||||
carry.value = 0;
|
||||
carry.pkPensionAnnual = 0;
|
||||
} else if (mode === "PENSION") {
|
||||
carry.pkPensionAnnual = floorToThousand((value * num(td.conversionRate)) / 100);
|
||||
carry.pkPensionAnnual = Math.round((value * num(td.conversionRate)) / 100);
|
||||
carry.value = 0;
|
||||
} else {
|
||||
const capital = Math.min(value, floorToThousand(num(td.capitalAmount)));
|
||||
const net = floorToThousand(capital * (1 - num(td.capitalTaxRate) / 100));
|
||||
const capital = Math.min(value, Math.round(num(td.capitalAmount)));
|
||||
const net = Math.round(capital * (1 - num(td.capitalTaxRate) / 100));
|
||||
outgoing += net;
|
||||
carry.pkPensionAnnual = floorToThousand(((value - capital) * num(td.conversionRate)) / 100);
|
||||
carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate)) / 100);
|
||||
carry.value = 0;
|
||||
}
|
||||
} else {
|
||||
const withdrawal = Math.min(ec.endValue, floorToThousand(num(td.withdrawal)));
|
||||
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
|
||||
carry.value = ec.endValue - withdrawal;
|
||||
outgoing += withdrawal;
|
||||
}
|
||||
@@ -445,11 +451,11 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
}
|
||||
case "PILLAR_3A": {
|
||||
if (ownerRetiresNext) {
|
||||
const net = floorToThousand(ec.endValue * (1 - num(td.capitalTaxRate) / 100));
|
||||
const net = Math.round(ec.endValue * (1 - num(td.capitalTaxRate) / 100));
|
||||
outgoing += net;
|
||||
carry.value = 0;
|
||||
} else {
|
||||
const withdrawal = Math.min(ec.endValue, floorToThousand(num(td.withdrawal)));
|
||||
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
|
||||
carry.value = ec.endValue - withdrawal;
|
||||
outgoing += withdrawal;
|
||||
}
|
||||
@@ -466,18 +472,18 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
}
|
||||
case "REAL_ESTATE": {
|
||||
if (td.decision === "SELL") {
|
||||
const purchase = floorToThousand(num(e.phaseValues[phase.id]?.purchasePrice));
|
||||
const salePrice = floorToThousand(num(td.salePrice));
|
||||
const purchase = Math.round(num(e.phaseValues[phase.id]?.purchasePrice));
|
||||
const salePrice = Math.round(num(td.salePrice));
|
||||
const gain = Math.max(0, salePrice - purchase);
|
||||
const tax = gain * (num(td.saleTaxRate) / 100);
|
||||
outgoing += floorToThousand(salePrice - carry.mortgage - tax);
|
||||
outgoing += Math.round(salePrice - carry.mortgage - tax);
|
||||
carry.status = "SOLD";
|
||||
}
|
||||
// HOLD: carry.mortgage bereits gesetzt.
|
||||
break;
|
||||
}
|
||||
case "OTHER_DEBT": {
|
||||
const immediate = Math.min(carry.owed, floorToThousand(num(td.immediateRepayment)));
|
||||
const immediate = Math.min(carry.owed, Math.round(num(td.immediateRepayment)));
|
||||
if (immediate > 0) {
|
||||
carry.owed = Math.max(0, carry.owed - immediate);
|
||||
outgoing -= immediate; // sofortige Tilgung mindert das verfuegbare Kapital
|
||||
@@ -491,7 +497,7 @@ export function computePlan(plan: PlanInput, household: HouseholdInput): PlanCom
|
||||
carry.hasCarry = true;
|
||||
}
|
||||
|
||||
incomingCapital = nextPhase ? floorToThousand(outgoing) : null;
|
||||
incomingCapital = nextPhase ? Math.round(outgoing) : null;
|
||||
yearsBefore += phase.durationYears;
|
||||
}
|
||||
|
||||
@@ -503,11 +509,10 @@ function personByRole(persons: { id: string; role: PersonRole }[], role: string)
|
||||
return persons.find((p) => p.role === role) ?? null;
|
||||
}
|
||||
|
||||
// Prueft, ob eine Person in der gegebenen Phase (mit gegebenem Jahres-Offset) pensioniert ist,
|
||||
// obwohl sie in der Vorphase noch erwerbend war.
|
||||
// Prueft, ob eine Person mit dem gegebenen Jahres-Offset (zu Beginn der Folgephase) pensioniert
|
||||
// ist, obwohl sie in der Vorphase noch erwerbend war.
|
||||
function retiresInPhase(
|
||||
personId: string,
|
||||
phase: { durationYears: number },
|
||||
persons: { id: string; role: PersonRole; age: number }[],
|
||||
retirementAge: Map<string, number>,
|
||||
yearsBeforeNext: number
|
||||
@@ -519,10 +524,6 @@ function retiresInPhase(
|
||||
return startAgeNext >= ra;
|
||||
}
|
||||
|
||||
function roundToHundred(v: number): number {
|
||||
return Math.round((v || 0) / 100) * 100;
|
||||
}
|
||||
|
||||
function fmt(v: number): string {
|
||||
const rounded = Math.round(v || 0);
|
||||
const sign = rounded < 0 ? "-" : "";
|
||||
|
||||
Reference in New Issue
Block a user