Szenario-Hierarchie: Plan als Behaelter, Diff-Markierung (V6)
Deploy App / deploy (push) Successful in 1m43s
Deploy App / deploy (push) Successful in 1m43s
Groesste Umstrukturierung bisher. Der PLAN ist neu ein schlanker Behaelter ohne Finanzdaten; die berechenbare Einheit ist das SZENARIO. Modell: - Jeder Plan bekommt beim Anlegen automatisch ein Basisszenario (isBase). - Das Grundprofil liegt am Szenario, nicht am Plan -- nur so sind Szenarien mit abweichendem PENSIONSALTER moeglich (Fruehpensionierung), das in Person steckt. - Neue Szenarien sind vollstaendige Kopien eines BELIEBIGEN Szenarios und haengen als Baum darunter (parentScenarioId); die Seitenleiste rueckt sie ein. - Kopierte Phasen/Elemente tragen Herkunfts-Verweise (sourcePhaseId, sourceElementId). Ueber den Namen zu matchen waere fragil gewesen. Abweichungs-Markierung (Diff gegen das Eltern-Szenario, live): - geaendert = gelb, neu = gruen + Badge, entfernt = graue Geisterzeile. - Markiert: Phasen-/Uebergangszellen, Element-Zeilen, Phasenkoepfe, Cash-Anfangswert, Cash-Uebergaenge, Grundprofil. Zaehler ueber der Matrix. - Eigene Theme-Tokens fuer Hell/Dunkel/Warm -- ein fester Gelbwert waere im Dunkelschema unbrauchbar. Charts vergleichen neu die Geschwister-Szenarien statt fremder Plaene. Datenmodell/Migration: - Neue Tabelle Plan; bisheriger Plan -> Scenario (IDs erhalten, damit alle Kind-Fremdschluessel gueltig bleiben); planId -> scenarioId in Person/Phase/ FinancialElement. Bestehende Szenarien werden per rekursivem CTE demselben Behaelter zugeordnet, auch mehrfach verschachtelte. - Migration VOR dem Deploy gegen echtes PostgreSQL verifiziert (PGlite, in-process), inkl. verschachtelter Szenarien und Cascade. Der Test ist als migrations.test.ts committet und sichert kuenftige Migrationen ab. API neu unter /api/scenarios/*. 10 neue Tests (48 -> 58). Spezifikation auf v0.8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,12 @@ interface PlanListItem {
|
||||
export function Dashboard({
|
||||
plan,
|
||||
computed,
|
||||
allPlans,
|
||||
siblings,
|
||||
}: {
|
||||
plan: PlanInput;
|
||||
computed: PlanComputed;
|
||||
allPlans: PlanListItem[];
|
||||
// Die uebrigen Szenarien desselben Plans -- nur die sind sinnvoll vergleichbar.
|
||||
siblings: PlanListItem[];
|
||||
}) {
|
||||
const [compareIds, setCompareIds] = useState<string[]>([]);
|
||||
const [compareData, setCompareData] = useState<Record<string, PlanComputed>>({});
|
||||
@@ -36,7 +37,7 @@ export function Dashboard({
|
||||
}
|
||||
setCompareIds((prev) => [...prev, id]);
|
||||
if (!compareData[id]) {
|
||||
const data = await api.get<{ computed: PlanComputed }>(`/api/plans/${id}`);
|
||||
const data = await api.get<{ computed: PlanComputed }>(`/api/scenarios/${id}`);
|
||||
setCompareData((prev) => ({ ...prev, [id]: data.computed }));
|
||||
}
|
||||
}
|
||||
@@ -45,11 +46,11 @@ export function Dashboard({
|
||||
const result: TimelineSeries[] = [{ label: plan.name, color: PALETTE[0], computed }];
|
||||
compareIds.forEach((id, i) => {
|
||||
const c = compareData[id];
|
||||
const name = allPlans.find((p) => p.id === id)?.name ?? id;
|
||||
const name = siblings.find((p) => p.id === id)?.name ?? id;
|
||||
if (c) result.push({ label: name, color: PALETTE[(i + 1) % PALETTE.length], computed: c });
|
||||
});
|
||||
return result;
|
||||
}, [plan.name, computed, compareIds, compareData, allPlans]);
|
||||
}, [plan.name, computed, compareIds, compareData, siblings]);
|
||||
|
||||
const ASSET_CATS = ["PENSION_FUND", "PILLAR_3A", "REAL_ESTATE", "OTHER_ASSET"];
|
||||
|
||||
@@ -87,7 +88,7 @@ export function Dashboard({
|
||||
[computed]
|
||||
);
|
||||
|
||||
const otherPlans = allPlans.filter((p) => p.id !== plan.id);
|
||||
const otherPlans = siblings;
|
||||
const lastPhase = computed.phases[computed.phases.length - 1];
|
||||
|
||||
return (
|
||||
@@ -117,7 +118,7 @@ export function Dashboard({
|
||||
Vermoegensverlauf nach Alter
|
||||
</h3>
|
||||
<a
|
||||
href={`/api/plans/${plan.id}/export`}
|
||||
href={`/api/scenarios/${plan.id}/export`}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-xs font-medium text-muted hover:bg-surface-2"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
@@ -126,7 +127,7 @@ export function Dashboard({
|
||||
</div>
|
||||
{otherPlans.length > 0 && (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
<span className="text-xs text-muted">Vergleichen mit:</span>
|
||||
<span className="text-xs text-muted">Szenarien vergleichen:</span>
|
||||
{otherPlans.map((p) => (
|
||||
<label key={p.id} className="flex items-center gap-1 text-xs text-muted">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user