import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, pct, fmtDatum, fmtMaand } from "../utils/format.js";
import { crPath } from "../utils/grafiek.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}
);
}
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
function MiniLineChart({ vData, fiatData, darkMode, T }) {
const [hovered, setHovered] = useState(null);
// pb was 52 voor gedraaide datumlabels. Die staan nu rechtop en zijn korter,
// dus er is minder hoogte nodig en houdt de lijn zelf meer ruimte over.
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 30;
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;
// Eén label "sep '26" is bij deze lettergrootte ongeveer 54px breed inclusief
// wat lucht ernaast. Zoveel passen er naast elkaar; de rest wordt
// overgeslagen. De stap telt vanaf het láátste punt terug, niet vanaf het
// eerste: dat punt is de datum waar je naar kijkt en moet er altijd bij staan.
// Forceerden we hem er los bovenop, dan botste hij op willekeurige afstand
// tegen zijn voorganger — precies wat er eerst misging.
const labelStap = Math.max(1, Math.ceil(vData.length / Math.max(1, Math.floor(iW / 54))));
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 (
);
}