Files
VibeFinance/frontend/src/pages/VoortgangTab.jsx
T
2026-09-17 16:23:39 +02:00

480 lines
25 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum, fmtMaand } from "../utils/format.js";
import { crPath, vlakPad } from "../utils/grafiek.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
import { RED, GREEN } from "../constants/index.js";
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
// ── Inline SVG line chart ─────────────────────────────────────────────────────
function SvgLineChart({ pts, lines, T, H = 200 }) {
const [hovered, setHovered] = useState(null); // { lineKey, ptIdx }
if (pts.length < 2) {
return (
<div style={{ color: T.muted, fontSize: 12, textAlign: "center", padding: "24px 0" }}>
Minimaal 2 datapunten nodig
</div>
);
}
// pb was 62 voor gedraaide datumlabels. Die staan nu rechtop en zijn korter.
const W = 700, pl = 64, pr = 16, pt2 = 16, pb = 34;
const iW = W - pl - pr, iH = H - pt2 - pb;
// Eén label "sep '26" is ongeveer 46px breed; zoveel passen er naast elkaar.
const labelStap = Math.max(1, Math.ceil(pts.length / Math.max(1, Math.floor(iW / 46))));
const allVals = lines.flatMap((l) => pts.map((p) => p[l.key] ?? 0));
const dataMin = Math.min(...allVals);
const dataMax = Math.max(...allVals);
const padding = (dataMax - dataMin) * 0.08 || 1;
const mn = dataMin - padding;
const mx = dataMax + padding;
const rng = mx - mn || 1;
const xp = (i) => pl + (i / (pts.length - 1)) * iW;
const yp = (v) => pt2 + (1 - ((v ?? 0) - mn) / rng) * iH;
// Was een vergelijking met een vaste hex (`T.bg === "#0f1117"`). Die kleur is
// de achtergrond allang niet meer, dus beide stonden altijd op licht — en met
// instelbare thema's valt er sowieso niets meer aan de bg af te lezen.
const darkMode = T.darkMode;
const gc = darkMode ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
return (
<div style={{ overflowX: "auto" }}>
<svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", minWidth: 340, height: "auto", display: "block" }}>
<defs>
{lines.map((l) => (
<linearGradient key={l.key} id={`g_${l.key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={l.color} stopOpacity="0.25" />
<stop offset="100%" stopColor={l.color} stopOpacity="0.02" />
</linearGradient>
))}
</defs>
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
const gy = pt2 + (1 - f) * iH;
return (
<g key={f}>
<line x1={pl} y1={gy} x2={W - pr} y2={gy} stroke={gc} strokeWidth="1" />
<text x={pl - 4} y={gy + 4} fill={T.muted} fontSize="8" textAnchor="end">
{fmt(mn + f * rng)}
</text>
</g>
);
})}
{lines.map((l) => {
const validPts = pts.map((p, i) => ({ ...p, _i: i })).filter((p) => p[l.key] !== null);
if (validPts.length < 2) return null;
const xs = validPts.map((p) => xp(p._i));
const ys = validPts.map((p) => yp(p[l.key]));
// Dezelfde Catmull-Rom afronding als de dashboardgrafiek, in plaats
// van rechte segmenten tussen de punten.
const lijn = crPath(xs, ys);
const vlak = vlakPad(lijn, xs[0], xs[xs.length - 1], pt2 + iH);
return (
<g key={l.key}>
<path d={vlak} fill={`url(#g_${l.key})`} />
<path d={lijn} fill="none" stroke={l.color} strokeWidth="2.5"
strokeLinejoin="round" strokeLinecap="round" />
{validPts.map((p, i) => {
const x = xs[i], y = ys[i];
const isHov = hovered?.lineKey === l.key && hovered?.ptIdx === i;
// Twee regels: sinds de x-as alleen nog maanden noemt, is dit
// de enige plek in de grafiek waar de exacte dag te lezen is.
const tipW = 104, tipH = 34;
const tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4);
const tipY = y - tipH - 8 < pt2 ? y + 10 : y - tipH - 8;
return (
<g key={i}
onMouseEnter={() => setHovered({ lineKey: l.key, ptIdx: i })}
onMouseLeave={() => setHovered(null)}
style={{ cursor: "pointer" }}
>
<circle cx={x} cy={y} r="14" fill="transparent" />
<circle cx={x} cy={y} r={isHov ? 6 : 4}
fill={l.color} stroke={T.card} strokeWidth="2" />
{isHov && (
<g>
<rect x={tipX} y={tipY} width={tipW} height={tipH}
rx="5" fill={T.card}
stroke={l.color} strokeWidth="1" />
<text x={tipX + tipW / 2} y={tipY + 13} fill={T.muted} fontSize="8"
textAnchor="middle">
{fmtDatum(p.datum)}
</text>
<text x={tipX + tipW / 2} y={tipY + 26} fill={l.color} fontSize="9"
fontWeight="700" textAnchor="middle">
{fmt(p[l.key])}
</text>
</g>
)}
</g>
);
})}
</g>
);
})}
{/* X-as: korte maandlabels, rechtop en uitgedund op wat er past. Elk
punt een volledige datum geven liep bij een lange reeks over in een
onleesbare band. De exacte dag staat in de tooltip op elk punt. */}
{pts.map((p, i) => {
const laatste = i === pts.length - 1;
if (i % labelStap !== 0 && !laatste) return null;
return (
<g key={i}>
<line x1={xp(i)} y1={pt2 + iH} x2={xp(i)} y2={pt2 + iH + 4}
stroke={T.muted} strokeOpacity="0.5" strokeWidth="1" />
<text x={xp(i)} y={pt2 + iH + 17} fill={T.muted} fontSize="9"
textAnchor={i === 0 ? "start" : laatste ? "end" : "middle"}>
{fmtMaand(p.datum)}
</text>
</g>
);
})}
</svg>
<div style={{ display: "flex", gap: 20, justifyContent: "center", marginTop: 10 }}>
{lines.map((l) => (
<div key={l.key} style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 18, height: 3, background: l.color, borderRadius: 2 }} />
<div style={{ width: 8, height: 8, borderRadius: "50%", background: l.color }} />
<span style={{ fontSize: 12, color: T.muted }}>{l.label}</span>
</div>
))}
</div>
</div>
);
}
// ── VoortgangModal ────────────────────────────────────────────────────────────
function VoortgangModal({ onSave, onClose, T, darkMode, initialData }) {
const is = { background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 8, color: T.text, padding: "8px 10px", fontSize: 13, outline: "none", boxSizing: "border-box" };
const isEdit = !!initialData;
const [datum, setDatum] = useState(initialData?.datum || "");
const [ev, setEv] = useState(initialData?.ev != null ? String(initialData.ev) : "");
const [schuld, setSchuld] = useState(initialData?.schuld != null ? String(initialData.schuld) : "");
const [opmerkingen, setOpmerkingen] = useState(initialData?.opmerkingen || "");
const [err, setErr] = useState("");
const save = () => {
if (!datum) { setErr("Vul een datum in."); setTimeout(() => setErr(""), 3000); return; }
if (!ev && !schuld) { setErr("Vul minimaal Eigen Vermogen of Schuld in."); setTimeout(() => setErr(""), 3000); return; }
onSave({
datum,
ev: ev ? parseFloat(ev) : null,
schuld: schuld ? parseFloat(schuld) : null,
opmerkingen,
});
onClose();
};
return (
<Overlay onClose={onClose}>
<ModalBox maxWidth={480}>
<ModalHeader
icon="📊" iconBg={T.accent}
title={isEdit ? "Voortgang bewerken" : "Voortgang toevoegen"}
subtitle="Vul de waarden in voor dit moment"
subtitleColor={T.muted}
onClose={onClose}
headerBg={darkMode ? `${T.accent}08` : T.inputBg}
T={T}
/>
<div style={{ padding: "20px 22px", overflowY: "auto", maxHeight: "70vh" }}>
<VLabel label="Datum *" T={T} />
<input type="date" value={datum}
onChange={(e) => setDatum(e.target.value)}
style={{ ...is, width: "100%", marginBottom: 16 }} />
<VLabel label="Eigen Vermogen (€)" T={T} />
<div style={{ position: "relative", marginBottom: 16 }}>
<span style={{ position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 12, color: T.muted, pointerEvents: "none" }}>€</span>
<input type="number" value={ev} onChange={(e) => setEv(e.target.value)}
placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
</div>
<VLabel label="Schuld (€)" T={T} />
<div style={{ position: "relative", marginBottom: 16 }}>
<span style={{ position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 12, color: T.muted, pointerEvents: "none" }}>€</span>
<input type="number" value={schuld} onChange={(e) => setSchuld(e.target.value)}
placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
</div>
<VLabel label="Opmerkingen" T={T} />
<input type="text" value={opmerkingen}
onChange={(e) => setOpmerkingen(e.target.value)}
placeholder="Optionele notitie..."
style={{ ...is, width: "100%" }} />
{err && (
<div style={{ color: RED, fontSize: 12, marginTop: 12, padding: "6px 10px", background: "rgba(239,68,68,0.1)", borderRadius: 8, border: "1px solid rgba(239,68,68,0.25)" }}>
{err}
</div>
)}
</div>
<ModalFooter T={T}>
<button onClick={onClose} style={{ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
Annuleren
</button>
<button onClick={save} style={{ flex: 2, padding: "10px", background: `linear-gradient(135deg, ${T.accent}, ${T.accent}cc)`, border: "none", borderRadius: 10, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 }}>
{isEdit ? "Opslaan" : "Toevoegen"}
</button>
</ModalFooter>
</ModalBox>
</Overlay>
);
}
function VLabel({ label, T }) {
return (
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 8 }}>
{label}
</div>
);
}
// ── VoortgangTab (pagina) ─────────────────────────────────────────────────────
export default function VoortgangTab() {
const [showAdd, setShowAdd] = useState(false);
const [editRow, setEditRow] = useState(null);
const {
totEV, totSchuld, catTotals, schuldBlokken, schuldHistory,
evVoortgangHistory, setEvVoortgangHistory,
data, setData, schuld, locked, darkMode, T,
} = useApp();
const nettoWaarde = totEV - totSchuld;
const schuldCatsDash = schuldBlokken.map((b) => ({
naam: b.naam, color: b.color,
subtot: b.items.reduce((a, i) => a + (i.waarde || 0), 0),
}));
const combined = [...new Set([
...evVoortgangHistory.map((r) => r.datum),
...schuldHistory.map((s) => s.datum),
])].sort((a, b) => a.localeCompare(b)).map((d) => {
const evRow = evVoortgangHistory.find((r) => r.datum === d);
const schRow = schuldHistory.find((s) => s.datum === d);
const evVal = evRow ? (evRow.ev || 0) : null;
const schVal = schRow ? schRow.schuld : null;
return {
datum: d,
ev: evVal,
schuld: schVal,
netto: evVal !== null && schVal !== null ? evVal - schVal : null,
opmerkingen: evRow?.opmerkingen || "",
};
});
const timeline = combined;
const voortgangRows = [...combined].reverse();
const addVoortgang = ({ datum, ev: evVal, schuld: schuldVal, opmerkingen }) => {
if (evVal !== null) {
const newRow = { datum, ev: evVal, opmerkingen: opmerkingen || "" };
const updated = [...evVoortgangHistory.filter((r) => r.datum !== datum), newRow]
.sort((a, b) => b.datum.localeCompare(a.datum));
setEvVoortgangHistory(updated);
}
if (schuldVal !== null) {
const updated = [...schuldHistory.filter((s) => s.datum !== datum), { datum, schuld: schuldVal }]
.sort((a, b) => b.datum.localeCompare(a.datum));
setData({ ...data, schuld: { ...schuld, schuldHistory: updated } });
}
};
const removeVoortgang = (datum) => {
setEvVoortgangHistory(evVoortgangHistory.filter((r) => r.datum !== datum));
setData({ ...data, schuld: { ...schuld, schuldHistory: schuldHistory.filter((s) => s.datum !== datum) } });
};
// Kolommen in weergave-volgorde, met hun standaardbreedte.
const kolommen = [
{ id: "datum", breedte: 120 },
{ id: "ev", breedte: 175 },
{ id: "schuld", breedte: 160 },
{ id: "netto", breedte: 175 },
{ id: "opmerkingen", breedte: 340 },
...(!locked ? [{ id: "acties", breedte: 90 }] : []),
];
const { breedtes, totaal, grip } = useKolomBreedtes("voortgang_kolom_breedtes", kolommen);
// overflow/ellipsis horen bij tableLayout "fixed": de inhoud rekt de kolom
// niet meer op, dus zonder afkappen loopt lange tekst de buurcel binnen.
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: T.popupBg, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
const td = { padding: "7px 10px", fontSize: 12, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", borderBottom: `1px solid ${T.border}` };
return (
<div>
<div style={{ marginBottom: 24 }}>
<h1 style={{ fontSize: 20, fontWeight: 800, margin: 0 }}>Voortgang Schuld & EV</h1>
</div>
{/* Hero netto */}
<VCard style={{
background: `linear-gradient(135deg, ${T.accent}22, rgba(16,185,129,0.08))`,
border: `1px solid ${T.border}`, borderLeft: `4px solid ${T.accent}`,
marginBottom: 16,
}}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
<div>
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6 }}>Mijn Nettowaarde</div>
<div style={{ fontSize: 32, fontWeight: 800, color: nettoWaarde >= 0 ? GREEN : RED }}>{fmt(nettoWaarde)}</div>
</div>
<div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: T.muted, marginBottom: 4, textTransform: "uppercase" }}>Eigen Vermogen</div>
<div style={{ fontSize: 20, fontWeight: 700, color: GREEN }}>{fmt(totEV)}</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: T.muted, marginBottom: 4, textTransform: "uppercase" }}>Totale Schuld</div>
<div style={{ fontSize: 20, fontWeight: 700, color: RED }}>{fmt(totSchuld)}</div>
</div>
</div>
</div>
</VCard>
{/* KPI kaarten */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(260px,1fr))", gap: 14, marginBottom: 20 }}>
{/* Schuld verdeling */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderTop: `3px solid ${RED}` }}>
<div style={{ fontWeight: 700, fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase", color: RED, marginBottom: 12 }}>Verdeling Schuld</div>
{schuldCatsDash.map((b) => (
<div key={b.naam} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${T.border}` }}>
<div style={{ display: "flex", alignItems: "center", gap: 7 }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: b.color }} />
<span style={{ fontSize: 13, color: T.muted }}>{b.naam}</span>
</div>
<span style={{ fontSize: 13, fontWeight: 600, color: b.color }}>{fmt(b.subtot)}</span>
</div>
))}
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 10, paddingTop: 8 }}>
<span style={{ fontSize: 12, fontWeight: 700, color: T.text }}>Totaal schuld</span>
<span style={{ fontSize: 15, fontWeight: 800, color: RED }}>{fmt(totSchuld)}</span>
</div>
</VCard>
{/* EV verdeling */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderTop: `3px solid ${GREEN}` }}>
<div style={{ fontWeight: 700, fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase", color: GREEN, marginBottom: 12 }}>Verdeling Eigen Vermogen</div>
{catTotals.map((c) => (
<div key={c.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: `1px solid ${T.border}` }}>
<div style={{ display: "flex", alignItems: "center", gap: 7 }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: c.color }} />
<span style={{ fontSize: 13, color: T.muted }}>{c.naam}</span>
</div>
<span style={{ fontSize: 13, fontWeight: 600, color: c.color }}>{fmt(c.tot)}</span>
</div>
))}
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 10, paddingTop: 8 }}>
<span style={{ fontSize: 12, fontWeight: 700, color: T.text }}>Totaal EV</span>
<span style={{ fontSize: 15, fontWeight: 800, color: GREEN }}>{fmt(totEV)}</span>
</div>
</VCard>
{/* Rendement */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderTop: `3px solid #f59e0b` }}>
<div style={{ fontWeight: 700, fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase", color: "#f59e0b", marginBottom: 12 }}>Rendement Eigen Vermogen</div>
<div style={{ padding: "10px 0", borderBottom: `1px solid ${T.border}` }}>
<div style={{ fontSize: 11, color: T.muted, marginBottom: 4 }}>Bedrag</div>
<div style={{ fontSize: 15, fontWeight: 700, color: T.muted }}>—</div>
</div>
<div style={{ padding: "10px 0" }}>
<div style={{ fontSize: 11, color: T.muted, marginBottom: 4 }}>Percentage</div>
<div style={{ fontSize: 15, fontWeight: 700, color: T.muted }}>—</div>
</div>
</VCard>
{/* Donut */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderRadius: 16, display: "flex", flexDirection: "column", alignItems: "center" }}>
<DonutChart segments={[{ val: totEV, color: GREEN, naam: "Eigen Vermogen" }, { val: totSchuld, color: RED, naam: "Schuld" }]} total={totEV + totSchuld || 1} size={200} label="Netto waarde" centerVal={fmt(totEV - totSchuld)} />
<div style={{ display: "flex", gap: 20, marginTop: 12, justifyContent: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 10, height: 10, borderRadius: "50%", background: GREEN }} />
<span style={{ fontSize: 12, color: T.subtext }}>Eigen Vermogen</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 10, height: 10, borderRadius: "50%", background: RED }} />
<span style={{ fontSize: 12, color: T.subtext }}>Schuld</span>
</div>
</div>
</VCard>
</div>
{/* Grafieken */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 14 }}>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 12 }}>Ontwikkeling Schuld en EV</div>
<SvgLineChart pts={timeline} lines={[
{ key: "ev", color: GREEN, label: "Eigen Vermogen" },
{ key: "schuld", color: RED, label: "Schuld" },
]} T={T} H={200} />
</VCard>
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 20 }}>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 12 }}>Ontwikkeling Mijn Nettowaarde</div>
<SvgLineChart pts={timeline} lines={[
{ key: "netto", color: GREEN, label: "Netto Vermogen" },
]} T={T} H={200} />
</VCard>
{/* Voortgang tabel */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 14 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div style={{ fontWeight: 700, fontSize: 14 }}>Voortgang overzicht</div>
{!locked && (
<button onClick={() => setShowAdd(true)} style={{
padding: "6px 14px", background: T.accent, border: `1px solid ${T.accent}`,
borderRadius: 8, color: "#fff", fontWeight: 700, fontSize: 12, cursor: "pointer",
}}>+ Toevoegen</button>
)}
</div>
{showAdd && !locked && (
<VoortgangModal onSave={addVoortgang} onClose={() => setShowAdd(false)} T={T} darkMode={darkMode} />
)}
{editRow && !locked && (
<VoortgangModal onSave={addVoortgang} onClose={() => setEditRow(null)} T={T} darkMode={darkMode} initialData={editRow} />
)}
{voortgangRows.length > 0 ? (
<div style={{ overflowX: "auto" }}>
<table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
<colgroup>
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
</colgroup>
<thead>
<tr>
<th style={th}>Datum<span {...grip("datum")} /></th>
<th style={{ ...th, color: GREEN }}>Eigen Vermogen<span {...grip("ev")} /></th>
<th style={{ ...th, color: RED }}>Schuld<span {...grip("schuld")} /></th>
<th style={{ ...th, color: T.accentTekst }}>Nettowaarde<span {...grip("netto")} /></th>
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
{!locked && <th style={th}><span {...grip("acties")} /></th>}
</tr>
</thead>
<tbody>
{voortgangRows.map((row, i) => (
<tr key={i}>
<td style={td}>{fmtDatum(row.datum)}</td>
<td style={{ ...td, color: GREEN, fontWeight: 600 }}>{row.ev !== null ? fmt(row.ev) : "—"}</td>
<td style={{ ...td, color: RED, fontWeight: 600 }}>{row.schuld !== null ? fmt(row.schuld) : "—"}</td>
<td style={{ ...td, fontWeight: 700, color: row.netto !== null ? (row.netto >= 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}</td>
{/* title: bij een smalle kolom is de tekst afgekapt */}
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
{!locked && (
<td style={{ ...td, whiteSpace: "nowrap" }}>
<button onClick={() => setEditRow(row)} style={{ padding: "2px 8px", background: `${T.accent}22`, border: `1px solid ${T.accent}44`, borderRadius: 6, color: T.accentTekst, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️</button>
<button onClick={() => removeVoortgang(row.datum)} style={{ padding: "2px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 12 }}>×</button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
) : (
<div style={{ color: T.muted, fontSize: 13, textAlign: "center", padding: "24px 0" }}>
Nog geen gecombineerde datapunten. Voeg schuld- en EV-momenten toe.
</div>
)}
</VCard>
</div>
);
}