2026-09-04 09:19:14 +02:00
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
import { useApp } from "../context/AppContext.jsx";
|
|
|
|
|
|
import { fmt, fmtDatum } from "../utils/format.js";
|
2026-09-10 14:03:34 +02:00
|
|
|
|
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
|
2026-09-15 14:02:17 +02:00
|
|
|
|
import { RED, GREEN } from "../constants/index.js";
|
2026-09-04 09:19:14 +02:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
const W = 700, pl = 64, pr = 16, pt2 = 16, pb = 62;
|
|
|
|
|
|
const iW = W - pl - pr, iH = H - pt2 - pb;
|
|
|
|
|
|
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;
|
2026-09-15 14:02:17 +02:00
|
|
|
|
// 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)";
|
2026-09-04 09:19:14 +02:00
|
|
|
|
|
|
|
|
|
|
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]));
|
|
|
|
|
|
const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" ");
|
|
|
|
|
|
const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<g key={l.key}>
|
|
|
|
|
|
<polygon points={ar} fill={`url(#g_${l.key})`} />
|
|
|
|
|
|
<polyline points={ls} 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;
|
|
|
|
|
|
const tipW = 90, tipH = 22;
|
|
|
|
|
|
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={22}
|
2026-09-15 14:02:17 +02:00
|
|
|
|
rx="5" fill={T.card}
|
2026-09-04 09:19:14 +02:00
|
|
|
|
stroke={l.color} strokeWidth="1" />
|
|
|
|
|
|
<text x={tipX + tipW / 2} y={tipY + 15} fill={l.color} fontSize="8"
|
|
|
|
|
|
fontWeight="700" textAnchor="middle">
|
|
|
|
|
|
{fmt(p[l.key])}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</g>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</g>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
{pts.map((p, i) => (
|
|
|
|
|
|
<text key={i}
|
|
|
|
|
|
x={xp(i)} y={H - pb + 16}
|
|
|
|
|
|
fill={T.muted} fontSize="8"
|
|
|
|
|
|
textAnchor="end"
|
|
|
|
|
|
transform={`rotate(-45, ${xp(i)}, ${H - pb + 16})`}>
|
|
|
|
|
|
{fmtDatum(p.datum)}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</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
|
2026-09-15 14:02:17 +02:00
|
|
|
|
icon="📊" iconBg={T.accent}
|
2026-09-04 09:19:14 +02:00
|
|
|
|
title={isEdit ? "Voortgang bewerken" : "Voortgang toevoegen"}
|
|
|
|
|
|
subtitle="Vul de waarden in voor dit moment"
|
|
|
|
|
|
subtitleColor={T.muted}
|
|
|
|
|
|
onClose={onClose}
|
2026-09-15 14:02:17 +02:00
|
|
|
|
headerBg={darkMode ? `${T.accent}08` : T.inputBg}
|
2026-09-04 09:19:14 +02:00
|
|
|
|
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>
|
2026-09-15 14:02:17 +02:00
|
|
|
|
<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 }}>
|
2026-09-04 09:19:14 +02:00
|
|
|
|
{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) } });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-10 14:03:34 +02:00
|
|
|
|
// 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}` };
|
2026-09-04 09:19:14 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ marginBottom: 24 }}>
|
|
|
|
|
|
<h1 style={{ fontSize: 20, fontWeight: 800, margin: 0 }}>Voortgang Schuld & EV</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hero netto */}
|
|
|
|
|
|
<VCard style={{
|
2026-09-15 14:02:17 +02:00
|
|
|
|
background: `linear-gradient(135deg, ${T.accent}22, rgba(16,185,129,0.08))`,
|
|
|
|
|
|
border: `1px solid ${T.border}`, borderLeft: `4px solid ${T.accent}`,
|
2026-09-04 09:19:14 +02:00
|
|
|
|
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" }}>
|
2026-09-04 09:19:23 +02:00
|
|
|
|
<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)} />
|
2026-09-04 09:19:14 +02:00
|
|
|
|
<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={{
|
2026-09-15 14:02:17 +02:00
|
|
|
|
padding: "6px 14px", background: T.accent, border: `1px solid ${T.accent}`,
|
2026-09-04 09:19:14 +02:00
|
|
|
|
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" }}>
|
2026-09-10 14:03:34 +02:00
|
|
|
|
<table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
|
|
|
|
|
|
<colgroup>
|
|
|
|
|
|
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
|
|
|
|
|
|
</colgroup>
|
2026-09-04 09:19:14 +02:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-09-10 14:03:34 +02:00
|
|
|
|
<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>
|
2026-09-15 14:02:17 +02:00
|
|
|
|
<th style={{ ...th, color: T.accentTekst }}>Nettowaarde<span {...grip("netto")} /></th>
|
2026-09-10 14:03:34 +02:00
|
|
|
|
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
|
|
|
|
|
|
{!locked && <th style={th}><span {...grip("acties")} /></th>}
|
2026-09-04 09:19:14 +02:00
|
|
|
|
</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>
|
2026-09-10 14:03:34 +02:00
|
|
|
|
{/* title: bij een smalle kolom is de tekst afgekapt */}
|
|
|
|
|
|
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
|
2026-09-04 09:19:14 +02:00
|
|
|
|
{!locked && (
|
|
|
|
|
|
<td style={{ ...td, whiteSpace: "nowrap" }}>
|
2026-09-15 14:02:17 +02:00
|
|
|
|
<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>
|
2026-09-04 09:19:14 +02:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|