Modul-Review 4: Matrix -- Kapitalverwendung, Phasendauer, Bedienung
Deploy App / deploy (push) Successful in 1m10s

Kapitalverwendung (Punkt C) am richtigen Ort:
- Prozent-Aufteilung des bezogenen Alterskapitals wandert vom Cash-Uebergang
  zum Bezugs-Entscheid der PK (nur bei Kapitalbezug) bzw. der Saeule 3a --
  mit zwei Guthaben liess sie sich vorher gar nicht getrennt beantworten
- Dialoge fuehren neu brutto -> Steuersatz -> netto -> Verteilung
- BUGFIX: Der zugeteilte Betrag erhoehte still den internen Bestand, deshalb
  zeigten Ziel-Element und "Kapital verteilen" eine 0. Er laeuft jetzt ueber
  Carry.capitalIn als Zusatzeinlage der Folgephase und ist ueberall sichtbar
- Saeule 3a ist am Pensions-Uebergang neu ein offener Entscheid

Phasendauer (gemeldeter Fehler):
- Die Folgephase gleicht eine geaenderte Dauer aus; Gesamtdauer bleibt gleich
- Vorher kappte das Tool nur die bearbeitete Phase -> Phase 2 ueberspannte
  danach die Pensionierung und die Invariante aus Punkt 44 kippte
- Rueckfrage vorher, Blockade wenn die Folgephase unter 1 Jahr fiele
- neue reine Funktion planDurationChange

Bedienung:
- Element-Zeile und Phasenkopf: Stift (umbenennen, beim Element inkl.
  Zuordnung), Papierkorb, Expand -- alle immer sichtbar
- PATCH /api/elements/<id> nimmt neu auch ownerRole
- Hilfetexte via Portal (wurden in scrollenden Dialogen abgeschnitten)
- Verteil-Dialoge: Zuordnung je Zeile, nach vom/ins Cash gruppiert,
  Vorbelegung mit dem EFFEKTIVEN Wert inkl. Vererbung (zeigte vorher 0)
- Matrix: gleiche Spaltenbreiten + horizontales Scrollen, "Alle auf-/
  zuklappen", Kategorie-Summe in der zugeklappten Zeile
- Phasen-Detailansicht nutzt die neue Aufteilungs-Grafik
- Uebersicht: "Leer starten" auch im leeren Zustand

Nebenbei: dritte verstuemmelte Hex-Farbe (#7c3aed) repariert, Phasen-Panel
nutzt den eigenen Bestaetigungs-Dialog statt window.confirm; mehrere veraltete
Referenzen und die buildCarryData-Tabelle in der Spez nachgezogen.

SPEZIFIKATION 0.33. 278 -> 288 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 23:09:03 +02:00
parent 0761f6b3e2
commit 0953880a80
17 changed files with 1040 additions and 267 deletions
+63
View File
@@ -48,3 +48,66 @@ export function defaultOpenDuration(persons: { age: number }[], fromYear: number
const oldestNow = persons.length > 0 ? Math.max(...persons.map((p) => p.age)) : 65;
return Math.max(5, 90 - (oldestNow + fromYear));
}
// --- Phasendauer ändern: die Folgephase gleicht aus --------------------------------------
//
// Wird eine Phase verlängert, muss die NÄCHSTE um denselben Betrag kürzer werden -- sonst
// verschieben sich alle folgenden Phasengrenzen, und eine davon überspannt am Ende eine
// Pensionierung. Genau das passierte bis 0.32: Die Kappung prüfte nur die BEARBEITETE Phase
// («Phase 1 darf höchstens bis zur Pensionierung laufen»), nicht die Folgen für Phase 2.
//
// Dieselbe Mechanik wie beim Verschieben des Pensionsalters (lib/retirement.ts): Die
// Gesamtdauer des Plans bleibt gleich, es wird nur Zeit umverteilt. Nur die LETZTE Phase hat
// keine Nachfolgerin -- sie verlängert oder verkürzt den Plan tatsächlich.
export interface DurationChange {
ok: boolean;
// Die Folgephase, die den Ausgleich trägt (null bei der letzten Phase).
neighbourId: string | null;
neighbourName: string | null;
neighbourNewDuration: number | null;
delta: number; // Änderung der bearbeiteten Phase in Jahren
error: string | null;
}
export function planDurationChange(
phases: { id: string; name: string; sequenceNumber: number; durationYears: number }[],
phaseId: string,
newDuration: number
): DurationChange {
const sorted = [...phases].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
const i = sorted.findIndex((p) => p.id === phaseId);
const none: DurationChange = {
ok: false, neighbourId: null, neighbourName: null, neighbourNewDuration: null, delta: 0, error: null,
};
if (i < 0) return { ...none, error: "Diese Lebensphase gibt es nicht." };
const delta = Math.round(newDuration) - sorted[i].durationYears;
if (newDuration < 1) return { ...none, delta, error: "Eine Lebensphase muss mindestens ein Jahr dauern." };
if (delta === 0) return { ...none, ok: true };
const next = sorted[i + 1];
// Letzte Phase: kein Ausgleich nötig, der Plan wird einfach länger oder kürzer.
if (!next) return { ...none, ok: true, delta };
const neighbourNewDuration = next.durationYears - delta;
if (neighbourNewDuration < 1) {
return {
...none,
delta,
error:
`«${next.name}» dauert nur ${next.durationYears} Jahr${next.durationYears === 1 ? "" : "e"} und müsste den ` +
`Ausgleich tragen. Verlängere diese Phase um höchstens ${next.durationYears - 1} Jahr` +
`${next.durationYears - 1 === 1 ? "" : "e"} oder passe zuerst die Folgephase an.`,
};
}
return {
ok: true,
neighbourId: next.id,
neighbourName: next.name,
neighbourNewDuration,
delta,
error: null,
};
}