Rework core model: financial elements as plan-wide entities across phases; derived phase types (Erwerb/Pension/Misch) with retirement-capped durations and per-plan retirement age; AHV (gap years + couple ceiling), PK payout/annuity, 3a, real estate, other assets/debts; horizontal timeline with retirement markers; phase x element matrix with detail panel; savings/consumption quota + available-capital key figures with red status
Deploy App / deploy (push) Successful in 1m57s

This commit is contained in:
2026-07-13 07:55:58 +02:00
parent b3a3b565ac
commit b775ab77cb
27 changed files with 2407 additions and 2166 deletions
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { Flag } from "lucide-react";
import type { PhaseComputed } from "@/lib/calculations";
interface PersonAxis {
role: "PERSON_A" | "PERSON_B";
label: string;
currentAge: number;
retirementAge: number;
color: string;
}
// Horizontale Zeitachse: Alter von links nach rechts, mit deutlich markiertem
// Pensionsalter je Person und Trennlinien an den Phasengrenzen.
export function Timeline({ phases, persons }: { phases: PhaseComputed[]; persons: PersonAxis[] }) {
if (phases.length === 0 || persons.length === 0) return null;
const totalYears = phases.reduce((s, p) => s + p.durationYears, 0);
const minAge = Math.min(...persons.map((p) => p.currentAge));
const maxAge = minAge + totalYears;
const span = Math.max(1, maxAge - minAge);
const pct = (age: number) => `${(Math.max(0, Math.min(span, age - minAge)) / span) * 100}%`;
// Phasengrenzen (kumulierte Jahre).
const boundaries: { year: number; label: string }[] = [];
let acc = 0;
for (const p of phases) {
boundaries.push({ year: acc, label: p.name });
acc += p.durationYears;
}
return (
<div className="rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">Zeitachse</h3>
<div className="flex gap-3 text-xs text-zinc-500">
{persons.map((p) => (
<span key={p.role} className="flex items-center gap-1">
<span className="inline-block h-2 w-2 rounded-full" style={{ backgroundColor: p.color }} />
{p.label} (heute {p.currentAge})
</span>
))}
</div>
</div>
<div className="relative pt-6">
{/* Pensionsmarker je Person */}
{persons.map((p, i) =>
p.retirementAge > minAge && p.retirementAge < maxAge ? (
<div
key={p.role}
className="absolute top-0 flex -translate-x-1/2 flex-col items-center"
style={{ left: pct(p.retirementAge) }}
title={`${p.label}: Pensionierung mit ${p.retirementAge}`}
>
<Flag className="h-3.5 w-3.5" style={{ color: p.color }} fill={p.color} />
<span className="whitespace-nowrap text-[10px] font-medium" style={{ color: p.color }}>
{p.retirementAge}
</span>
<div className="mt-0.5 h-3 w-px" style={{ backgroundColor: p.color, marginTop: i * 2 }} />
</div>
) : null
)}
{/* Achse */}
<div className="relative h-2 w-full rounded-full bg-gradient-to-r from-indigo-200 to-indigo-400 dark:from-indigo-500/30 dark:to-indigo-500/60">
{boundaries.slice(1).map((b) => (
<div
key={b.year}
className="absolute top-0 h-2 w-px bg-white/70 dark:bg-zinc-900/70"
style={{ left: pct(minAge + b.year) }}
/>
))}
</div>
{/* Alters-Beschriftung */}
<div className="mt-1 flex justify-between text-[11px] text-zinc-500">
<span>{minAge} J.</span>
<span>{maxAge} J.</span>
</div>
</div>
</div>
);
}