Plan-Assistent: abschnittsbasierte Phasenplanung + Vermoegen pro Person
Deploy App / deploy (push) Successful in 58s

Rein an der Oberflaeche, keine Aenderung an Berechnung, Datenmodell oder API.

Schritt 2 (Lebensphasen) an den fixen Pensionierungszeitpunkten ausgerichtet:
- neues reines Modul phaseplan.ts (planSegments) leitet die Abschnitte ab:
  Erwerb (alle arbeiten) / Misch (eine pensioniert, eine arbeitet) / Pension
  (alle pensioniert), je mit kurzer Definition
- feste Abschnitte (durch Pensionierung begrenzt): beliebig viele Phasen mit
  +/Papierkorb und eigenem Namen; Live-Summe erzwingt exaktes Aufgehen,
  "Weiter" bis dahin gesperrt
- offener Pensions-Abschnitt: Dauer frei
- Anzahl Abschnitte wird abgeleitet (Einzel: 2, Paar unterschiedl. Ret: 3,
  bereits pensioniert: Misch/Pension zuerst)
- neue Zeitachse mit Pensionierungs-Flaggen; Phasen nummeriert und UNTER dem
  Balken beschriftet (kurze Phasen bleiben lesbar)
- behebt den Fehler, dass die Erwerbsphase beliebig ueber die Pensionierung
  hinaus gesetzt werden konnte

Schritt 4 (Vorsorge & Vermoegen) bei Paaren aufgeteilt in
Gemeinsam / Person A / Person B; PK und 3a je Person, Wertschriften/
Wohneigentum/Schulden je Bereich.

8 Tests (111 -> 119). SPEZIFIKATION auf 0.15 (Kap. 3.2.8 neu gefasst).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 13:32:43 +02:00
parent 9252f7188d
commit 1d046e9f7e
4 changed files with 613 additions and 195 deletions
+50
View File
@@ -0,0 +1,50 @@
// Ableitung der Lebensabschnitts-"Teile" aus den fixen Pensionierungszeitpunkten.
//
// Die Pensionsalter der Personen sind Fixpunkte auf der Lebenslinie. Zwischen Planbeginn und
// diesen Punkten entstehen Abschnitte mit konstantem Erwerbsstatus -- Erwerb (alle arbeiten),
// Misch (eine Person pensioniert, eine arbeitet) oder Pension (alle pensioniert). Genau diese
// Abschnitte muss der Assistent respektieren: Eine Phase darf keinen Pensionierungspunkt
// überspannen (die Berechnung leitet den Phasentyp am Phasenbeginn ab und kappt entsprechend,
// siehe SPEZIFIKATION 2.3 / maxPhaseDuration).
//
// Reine Funktion, ohne UI und ohne I/O -- nur vom Assistenten verwendet.
export type SegmentType = "ERWERB" | "MIXED" | "PENSION";
export interface PlanSegment {
type: SegmentType;
fromYear: number; // Jahre ab Planbeginn (Beginn des Abschnitts)
// Feste Länge in Jahren (durch den nächsten Pensionierungspunkt bestimmt) ODER null für den
// letzten, OFFENEN Abschnitt (Pension bis Lebensende -- Dauer frei wählbar).
fixedYears: number | null;
}
// Ein Person-Erwerbsstatus wechselt genau einmal: im Jahr `retirementAge - age` (auf 0 gekappt,
// falls bei Planbeginn bereits pensioniert). Die distinkten dieser Zeitpunkte sind die
// Abschnittsgrenzen; der Abschnitt nach dem letzten Zeitpunkt ist offen.
export function planSegments(persons: { age: number; retirementAge: number }[]): PlanSegment[] {
if (persons.length === 0) return [{ type: "PENSION", fromYear: 0, fixedYears: null }];
const retYears = persons.map((p) => Math.max(0, p.retirementAge - p.age));
const events = [...new Set(retYears.filter((r) => r > 0))].sort((a, b) => a - b);
const boundaries = [0, ...events];
const segments: PlanSegment[] = [];
for (let i = 0; i < boundaries.length; i++) {
const from = boundaries[i];
const next = i + 1 < boundaries.length ? boundaries[i + 1] : null;
// Eine Person arbeitet in einem Abschnitt, der bei Jahr `from` beginnt, genau dann, wenn
// ihr Pensionierungszeitpunkt echt später liegt.
const working = retYears.filter((r) => r > from).length;
const type: SegmentType = working === persons.length ? "ERWERB" : working > 0 ? "MIXED" : "PENSION";
segments.push({ type, fromYear: from, fixedYears: next === null ? null : next - from });
}
return segments;
}
// Vorgeschlagene Dauer des offenen Pensions-Abschnitts: bis das älteste Haushaltsmitglied
// ungefähr 90 ist, mindestens aber 5 Jahre.
export function defaultOpenDuration(persons: { age: number }[], fromYear: number): number {
const oldestNow = persons.length > 0 ? Math.max(...persons.map((p) => p.age)) : 65;
return Math.max(5, 90 - (oldestNow + fromYear));
}