Files
FPT/src/components/PhaseDetail.tsx
T
admGitAICDS 0953880a80
Deploy App / deploy (push) Successful in 1m10s
Modul-Review 4: Matrix -- Kapitalverwendung, Phasendauer, Bedienung
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>
2026-07-25 23:09:03 +02:00

142 lines
5.3 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.
"use client";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { NumberField, TextField } from "@/components/FormField";
import { CarryWarning } from "@/components/ElementDetail";
import { Button, useConfirm, useToast } from "@/components/ui";
import { api } from "@/lib/api-client";
import { planDurationChange } from "@/lib/phaseplan";
import type { PhaseInput } from "@/lib/types";
export function PhaseDetail({
phase,
allPhases,
maxDurationYears,
isLast,
laterPhaseCount,
onSaved,
onDeleted,
}: {
phase: PhaseInput;
// Alle Phasen des Szenarios -- nötig, um die Auswirkung einer geänderten Dauer auf die
// Folgephase VORHER zu zeigen (siehe planDurationChange).
allPhases: PhaseInput[];
maxDurationYears: number | null;
isLast: boolean;
laterPhaseCount: number;
onSaved: () => void;
onDeleted: () => void;
}) {
const [name, setName] = useState(phase.name);
const [durationYears, setDurationYears] = useState(phase.durationYears);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const confirm = useConfirm();
const toast = useToast();
const cap = maxDurationYears;
// Vorschau: Was passiert mit der Folgephase? Wird live berechnet, damit die Auswirkung
// sichtbar ist, BEVOR gespeichert wird.
const change = planDurationChange(allPhases, phase.id, durationYears);
async function save() {
// Verschiebt sich eine Folgephase, wird das vorher bestätigt -- sonst ändert ein Klick
// stillschweigend zwei Phasen.
if (change.ok && change.neighbourId && change.delta !== 0) {
const jahre = Math.abs(change.delta);
const ok = await confirm({
title: "Folgephase wird angepasst",
message:
`Wenn du «${phase.name}» um ${jahre} Jahr${jahre === 1 ? "" : "e"} ` +
`${change.delta > 0 ? "verlängerst" : "verkürzst"}, wird «${change.neighbourName}» automatisch auf ` +
`${change.neighbourNewDuration} Jahr${change.neighbourNewDuration === 1 ? "" : "e"} ` +
`${change.delta > 0 ? "gekürzt" : "verlängert"} die Gesamtdauer des Plans bleibt gleich. ` +
`Ohne diesen Ausgleich würde eine spätere Lebensphase eine Pensionierung überspannen.`,
confirmLabel: "Fortfahren",
});
if (!ok) return;
}
setSaving(true);
setError(null);
try {
await api.put(`/api/phases/${phase.id}`, { name, durationYears });
onSaved();
} catch (e) {
setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen.");
} finally {
setSaving(false);
}
}
async function remove() {
const ok = await confirm({
title: "Lebensphase löschen?",
message: ${phase.name}» wird mit allen erfassten Werten und Übergangs-Entscheiden entfernt.`,
confirmLabel: "Endgültig löschen",
danger: true,
});
if (!ok) return;
try {
await api.delete(`/api/phases/${phase.id}`);
onDeleted();
} catch (e) {
toast("error", e instanceof Error ? e.message : "Löschen fehlgeschlagen.");
}
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold uppercase tracking-wide text-accent-soft-fg">Lebensphase</div>
{isLast && (
<button
type="button"
onClick={remove}
className="flex items-center gap-1 rounded-lg border border-border px-2 py-1 text-xs text-muted hover:border-danger hover:bg-danger-soft hover:text-danger"
>
<Trash2 className="h-3.5 w-3.5" /> Phase löschen
</button>
)}
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{/* Eine geänderte Dauer verschiebt alle Folgephasen (Alter, Renten, Vermögen). */}
<CarryWarning laterPhaseCount={laterPhaseCount} />
<TextField label="Bezeichnung" value={name} onChange={setName} />
<NumberField
label={`Dauer (Jahre)${cap != null ? ` · max. ${cap}` : ""}`}
help={cap != null ? "Die Dauer ist ans nächste Pensionsereignis gekappt." : undefined}
value={durationYears}
min={1}
max={cap ?? undefined}
onChange={(v) => setDurationYears(cap != null ? Math.min(v, cap) : v)}
/>
</div>
{/* Auswirkung auf die Folgephase -- sichtbar, bevor gespeichert wird. */}
{change.error && (
<p className="rounded-lg border border-danger bg-danger-soft px-3 py-2 text-xs text-danger">{change.error}</p>
)}
{change.ok && change.neighbourId && change.delta !== 0 && (
<p className="rounded-lg border border-attention bg-attention-soft px-3 py-2 text-xs text-attention-soft-fg">
«{change.neighbourName}» wird dadurch auf <strong>{change.neighbourNewDuration} Jahre</strong>{" "}
{change.delta > 0 ? "gekürzt" : "verlängert"} die Gesamtdauer des Plans bleibt gleich.
</p>
)}
<p className="text-xs text-faint">
Die Inflationsrate gilt für das ganze Szenario und wird im Grundprofil gesetzt.
</p>
{error && <p className="text-sm text-danger">{error}</p>}
<div>
<Button disabled={saving || !!change.error} onClick={save}>
{saving ? "Speichern…" : "Speichern"}
</Button>
</div>
</div>
);
}