480 lines
25 KiB
React
480 lines
25 KiB
React
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>
|
||
);
|
||
}
|