Initial commit: FPT Financial Planning Tool
Deploy App / deploy (push) Successful in 3m3s

This commit is contained in:
2026-07-08 19:19:39 +02:00
commit 4f990c9686
60 changed files with 12436 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
"use client";
import {
CartesianGrid,
Legend,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { PlanComputed } from "@/lib/calculations";
export interface TimelineSeries {
label: string;
color: string;
computed: PlanComputed;
}
function buildTimeline(computed: PlanComputed) {
const points: { year: number; nominal: number; real: number }[] = [
{ year: 0, nominal: computed.phases[0]?.startWealthNominal ?? 0, real: computed.phases[0]?.startWealthNominal ?? 0 },
];
const boundaries: { year: number; name: string }[] = [];
let year = 0;
for (const phase of computed.phases) {
boundaries.push({ year, name: phase.name });
for (let y = 0; y < phase.durationYears; y++) {
year += 1;
points.push({ year, nominal: phase.yearlyNominal[y], real: phase.yearlyReal[y] });
}
}
return { points, boundaries };
}
// Liniendiagramm ueber alle Phasen, nominal + real, mit Markierungen an den
// Phasengrenzen (TDD Kapitel 4.5 / 14). Unterstuetzt optional mehrere ueberlagerte
// Plaene fuer den Szenario-Vergleich.
export function WealthChart({ series }: { series: TimelineSeries[] }) {
if (series.length === 0 || series[0].computed.phases.length === 0) {
return <p className="text-sm text-zinc-500">Noch keine Phasen vorhanden.</p>;
}
const primary = buildTimeline(series[0].computed);
const maxYear = Math.max(...series.map((s) => buildTimeline(s.computed).points.length - 1));
const merged: Record<number, Record<string, number>> = {};
for (const s of series) {
const tl = buildTimeline(s.computed);
for (const p of tl.points) {
merged[p.year] = merged[p.year] ?? { year: p.year };
merged[p.year][`${s.label} (nominal)`] = p.nominal;
merged[p.year][`${s.label} (real)`] = p.real;
}
}
const data = Array.from({ length: maxYear + 1 }, (_, y) => merged[y] ?? { year: y });
return (
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-zinc-200 dark:stroke-zinc-700" />
<XAxis dataKey="year" tick={{ fontSize: 11 }} label={{ value: "Jahr", position: "insideBottomRight", offset: -4, fontSize: 11 }} />
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
/>
<Tooltip
formatter={(v) =>
typeof v === "number" ? v.toLocaleString("de-CH", { maximumFractionDigits: 0 }) : v
}
/>
<Legend wrapperStyle={{ fontSize: 12 }} />
{primary.boundaries.slice(1).map((b) => (
<ReferenceLine key={b.year} x={b.year} stroke="#a1a1aa" strokeDasharray="2 2" />
))}
{series.map((s) => (
<Line
key={`${s.label}-nominal`}
type="monotone"
dataKey={`${s.label} (nominal)`}
stroke={s.color}
strokeWidth={2}
dot={false}
/>
))}
{series.map((s) => (
<Line
key={`${s.label}-real`}
type="monotone"
dataKey={`${s.label} (real)`}
stroke={s.color}
strokeWidth={2}
strokeDasharray="5 3"
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
}