import { useState } from "react"; import { useApp } from "../context/AppContext.jsx"; import { fmt, pct, fmtDatum } from "../utils/format.js"; import { GREEN, RED, GOLD } 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: T.accentTekst}, { label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: T.accentTekst}, { label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: T.accentTekst}, { 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)); // Cumulatief geïnvesteerde fiat, op dezelfde datums als vData. Dezelfde // opbouw als de KPI's hierboven: het oudste punt is de start-EV (daar is de // inleg per definitie gelijk aan de waarde), elk volgend punt telt de // stortingen van die rij erbij op. Het laatste punt is daarmee gelijk aan // "Totaal geïnvesteerde fiat". const fiatData = vData.reduce((acc, row, i) => { acc.push(i === 0 ? (row.nettoWaarde || 0) : acc[i - 1] + catSum(row)); return acc; }, []); return (
Hi{voornaam ? ` ${voornaam},` : ","}

{dagdeel} 👋

The Road naar {fmt(doel)}
{/* KPI grid */}
{kpis.map(({ label, val, accent }) => (
{label}
{val}
))}
{/* Doel progressie */}
🎯 Doel & Bijbehorende Progressie
{pct(progressPct)} behaald
{[0.25, 0.5, 0.75].map((f) => (
))}
{milestones.map((m) => ( {fmt(m)} ))}
{/* EV Grafiek */}
Ontwikkeling van Eigen Vermogen
{vData.length >= 2 && (
)}
{vData.length < 2 ? (
📈
Leg minimaal 2 voortgangsmomenten vast in het Eigen Vermogen tabblad om de grafiek te tonen.
) : ( )}
); } // ── Legenda-item bij de grafiek ───────────────────────────────────────────── function Legenda({ kleur, label }) { return ( {label} ); } // ── Smooth Catmull-Rom bezier path ────────────────────────────────────────── function crPath(xs, ys, t = 0.4) { if (xs.length < 2) return ""; let d = `M ${xs[0]},${ys[0]}`; for (let i = 0; i < xs.length - 1; i++) { const p0 = i > 0 ? [xs[i-1], ys[i-1]] : [xs[i], ys[i]]; const p1 = [xs[i], ys[i]]; const p2 = [xs[i+1], ys[i+1]]; const p3 = i < xs.length - 2 ? [xs[i+2], ys[i+2]] : [xs[i+1], ys[i+1]]; const cp1x = (p1[0] + (p2[0] - p0[0]) * t).toFixed(2); const cp1y = (p1[1] + (p2[1] - p0[1]) * t).toFixed(2); const cp2x = (p2[0] - (p3[0] - p1[0]) * t).toFixed(2); const cp2y = (p2[1] - (p3[1] - p1[1]) * t).toFixed(2); d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`; } return d; } // ── Mini inline SVG grafiek ──────────────────────────────────────────────────── function MiniLineChart({ vData, fiatData, darkMode, T }) { const [hovered, setHovered] = useState(null); const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52; const vals = vData.map((d) => d.nettoWaarde || 0); const fVals = fiatData; // Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei // omvatten — anders loopt de inleglijn buiten beeld zodra die boven de EV uitkomt. const mn = Math.min(...vals, ...fVals) * 0.95; const mx = Math.max(...vals, ...fVals) * 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 fYs = fVals.map((v) => pt + (1 - (v - mn) / rng) * iH); const linePath = crPath(xs, ys); const areaPath = `${linePath} L ${xs[xs.length-1]},${pt+iH} L ${xs[0]},${pt+iH} Z`; // Rechte segmenten, geen crPath: de inleg loopt nooit terug, en de bezier- // afronding zou tussen twee stortingen een dip tekenen die er niet is. const fiatPath = xs.map((x, i) => `${i ? "L" : "M"} ${x},${fYs[i]}`).join(" "); const gc = darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)"; const lc = T.muted; const handleMouseMove = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const svgX = ((e.clientX - rect.left) / rect.width) * W; const dists = xs.map((x) => Math.abs(x - svgX)); setHovered(dists.indexOf(Math.min(...dists))); }; const h = hovered; const tipW = 150, tipH = 76; // Náást de hoverlijn, niet erboven of eronder. Zodra de twee punten ver uit // elkaar liggen is er verticaal geen ruimte voor een kaart van 76px: dan // klapte hij naar onderen tot buiten de tekening, over de datumlabels heen en // half afgekapt. Zijwaarts past hij altijd; de klemmen houden hem binnen het // tekenvlak. const naarRechts = h !== null && xs[h] < pl + iW / 2; const tipX = h === null ? 0 : naarRechts ? Math.min(xs[h] + 16, W - pr - tipW) : Math.max(xs[h] - tipW - 16, pl); const tipY = h === null ? 0 : Math.max(pt, Math.min((ys[h] + fYs[h]) / 2 - tipH / 2, pt + iH - tipH)); const resultaat = h !== null ? vals[h] - fVals[h] : 0; return ( setHovered(null)} > {/* Gestippelde gridlijnen */} {[0, 0.25, 0.5, 0.75, 1].map((f) => { const gy = pt + (1 - f) * iH; return ( {fmt(mn + f * rng)} ); })} {/* Vlakkleur met gradient + fade */} {/* Geïnvesteerde fiat — gestippeld, zodat het als referentielijn leest en de EV-lijn de hoofdrol houdt */} {/* Lijn */} {/* Verticale hover-lijn */} {h !== null && ( )} {/* Hover punten */} {h !== null && ( <> )} {/* Tooltip */} {h !== null && ( {fmtDatum(vData[h].datum)} Vermogen {fmt(vals[h])} Ingelegd {fmt(fVals[h])} Resultaat = 0 ? GREEN : RED} fontSize="10" fontWeight="700" textAnchor="end"> {`${resultaat >= 0 ? "+" : ""}${fmt(resultaat)}`} )} {/* X-as labels */} {xs.map((x, i) => ( {fmtDatum(vData[i].datum)} ))} ); }