Files
VibeFinance/frontend/src/pages/DashboardTab.jsx
2026-04-16 10:22:13 +02:00

210 lines
8.6 KiB
JavaScript

import { useApp } from "../context/AppContext.jsx";
import { fmt, pct, fmtDatum } from "../utils/format.js";
import { PURPLE, PURPLE_LIGHT, GREEN, RED } from "../constants/index.js";
import { VCard, VBadge } from "../components/ui/index.jsx";
export default function DashboardTab() {
const { ev, totEV, evRows, cats, T, darkMode, currentUser } = useApp();
const hour = new Date().getHours();
const dagdeel = hour < 12 ? "Goedemorgen" : hour < 18 ? "Goedemiddag" : "Goedenavond";
const rawNaam = currentUser?.naam?.trim().split(/\s+/)[0] || "";
const voornaam = rawNaam ? rawNaam.charAt(0).toUpperCase() + rawNaam.slice(1) : "";
const catSum = (row) =>
cats.reduce((a, c) => a + (row[c.id] || 0), 0);
const startEV = evRows.length > 0 ? (evRows[evRows.length - 1]?.nettoWaarde || 0) : 0;
const extraFiat = evRows.length > 1
? evRows.slice(0, -1).reduce((acc, row) => acc + catSum(row), 0) : 0;
const totaalFiat = startEV + extraFiat;
const winst = totEV - totaalFiat;
const roi = totaalFiat > 0 ? winst / totaalFiat : 0;
const doel = ev.doel || 1;
const progressPct = Math.min(totEV / doel, 1);
const milestones = [0, 0.25, 0.5, 0.75, 1].map((f) => f * doel);
const kpis = [
{ label: "Start eigen vermogen", val: fmt(startEV), accent: PURPLE },
{ label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: PURPLE },
{ label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: PURPLE },
{ label: "Totaal eigen vermogen", val: fmt(totEV), accent: GREEN },
{ label: "Winst op investering", val: fmt(winst), accent: winst >= 0 ? GREEN : RED },
{ label: "ROI %", val: totaalFiat > 0 ? pct(roi, 2) : "—", accent: roi >= 0 ? GREEN : RED },
];
// Minigrafiek data
const vData = [...evRows].sort((a, b) => a.datum.localeCompare(b.datum));
return (
<div>
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 28, fontWeight: 800, color: T.text, marginBottom: 32 }}>
{dagdeel}{voornaam ? `, ${voornaam}` : ""} 👋
</div>
<h1 style={{ fontSize: 22, fontWeight: 800, margin: 0 }}>
The Road naar {fmt(doel)}
</h1>
<div style={{ color: T.muted, fontSize: 13, marginTop: 2 }}>
Persoonlijk vermogensdashboard
</div>
</div>
{/* KPI grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 12, marginBottom: 20,
}}>
{kpis.map(({ label, val, accent }) => (
<VCard key={label} style={{
background: T.card, border: `1px solid ${T.border}`,
borderLeft: `3px solid ${accent}44`,
}}>
<div style={{
fontSize: 11, color: T.muted, marginBottom: 6,
textTransform: "uppercase", letterSpacing: "0.05em",
}}>{label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: accent }}>{val}</div>
</VCard>
))}
</div>
{/* Doel progressie */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 16 }}>
<div style={{
display: "flex", alignItems: "center",
justifyContent: "space-between", marginBottom: 12,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ fontSize: 16 }}>🎯</span>
<span style={{ fontWeight: 600 }}>Doel &amp; Bijbehorende Progressie</span>
<VBadge label={fmt(doel)} color={PURPLE} />
</div>
<span style={{ color: T.muted, fontSize: 13 }}>{pct(progressPct)} behaald</span>
</div>
<div style={{
height: 14, background: "rgba(255,255,255,0.07)",
borderRadius: 99, overflow: "hidden",
position: "relative", marginBottom: 8,
}}>
<div style={{
position: "absolute", inset: 0,
width: `${progressPct * 100}%`,
background: `linear-gradient(90deg,${PURPLE},#a855f7)`,
borderRadius: 99, transition: "width 0.7s ease",
}} />
{[0.25, 0.5, 0.75].map((f) => (
<div key={f} style={{
position: "absolute", left: `${f * 100}%`,
top: 0, bottom: 0, width: 1,
background: "rgba(0,0,0,0.3)",
}} />
))}
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
{milestones.map((m) => (
<span key={m} style={{ fontSize: 11, color: T.muted }}>{fmt(m)}</span>
))}
</div>
</VCard>
{/* EV Grafiek */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 20 }}>
<div style={{ fontWeight: 600, marginBottom: 16 }}>Ontwikkeling van Eigen Vermogen</div>
{vData.length < 2 ? (
<div style={{ textAlign: "center", padding: "32px 0", color: T.muted }}>
<div style={{ fontSize: 28, marginBottom: 8 }}>📈</div>
<div style={{ fontSize: 13 }}>
Leg minimaal 2 voortgangsmomenten vast in het Eigen Vermogen tabblad om de grafiek te tonen.
</div>
</div>
) : (
<MiniLineChart vData={vData} darkMode={darkMode} />
)}
</VCard>
</div>
);
}
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
function MiniLineChart({ vData, darkMode }) {
const [hovered, setHovered] = useState(null);
const W = 700, H = 230, pl = 58, pr = 16, pt = 12, pb = 62;
const vals = vData.map((d) => d.nettoWaarde || 0);
const mn = Math.min(...vals) * 0.95;
const mx = Math.max(...vals) * 1.05;
const rng = mx - mn || 1;
const iW = W - pl - pr, iH = H - pt - pb;
const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW);
const ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH);
const line = xs.map((x, i) => `${x},${ys[i]}`).join(" ");
const area = `${xs[0]},${pt + iH} ${line} ${xs[xs.length - 1]},${pt + iH}`;
const gc = darkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
const lc = darkMode ? "#6b7280" : "#94a3b8";
return (
<svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: "auto", display: "block" }}>
<defs>
<linearGradient id="evDashFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={PURPLE} stopOpacity="0.35" />
<stop offset="100%" stopColor={PURPLE} stopOpacity="0.02" />
</linearGradient>
</defs>
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
const gy = pt + (1 - f) * iH;
return (
<g key={f}>
<line x1={pl} y1={gy} x2={W - pr} y2={gy} stroke={gc} strokeWidth="1" />
<text x={pl - 5} y={gy + 4} fill={lc} fontSize="9" textAnchor="end">
{fmt(mn + f * rng)}
</text>
</g>
);
})}
<polygon points={area} fill="url(#evDashFill)" />
<polyline points={line} fill="none" stroke={PURPLE_LIGHT}
strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round" />
{xs.map((x, i) => {
const isHov = hovered === i;
const tipW = 90, tipH = 22;
const tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4);
const tipY = ys[i] - tipH - 8 < pt ? ys[i] + 10 : ys[i] - tipH - 8;
return (
<g key={i}
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
style={{ cursor: "pointer" }}
>
{/* Vergroot klikgebied */}
<circle cx={x} cy={ys[i]} r="14" fill="transparent" />
<circle cx={x} cy={ys[i]} r={isHov ? 6 : 4}
fill={PURPLE_LIGHT}
stroke={darkMode ? "#1a1d27" : "#ffffff"}
strokeWidth="2" />
<text
x={x} y={H - pb + 16} fill={lc} fontSize="8"
textAnchor="end"
transform={`rotate(-45, ${x}, ${H - pb + 16})`}
>
{fmtDatum(vData[i].datum)}
</text>
{/* Tooltip */}
{isHov && (
<g>
<rect x={tipX} y={tipY} width={tipW} height={22}
rx="5" fill={darkMode ? "#1e2130" : "#fff"}
stroke={PURPLE} strokeWidth="1" />
<text x={tipX + tipW / 2} y={tipY + 15} fill={PURPLE_LIGHT} fontSize="8"
fontWeight="700" textAnchor="middle">
{fmt(vals[i])}
</text>
</g>
)}
</g>
);
})}
</svg>
);
}