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
Deploy App / deploy (push) Successful in 1m57s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user