import { useState } from "react"; import { useApp } from "../context/AppContext.jsx"; import { fmt, fmtDatum } from "../utils/format.js"; import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js"; import { PURPLE, PURPLE_LIGHT, 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 (
Minimaal 2 datapunten nodig
); } 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; const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"; const darkMode = T.bg === "#0f1117"; return (
{lines.map((l) => ( ))} {[0, 0.25, 0.5, 0.75, 1].map((f) => { const gy = pt2 + (1 - f) * iH; return ( {fmt(mn + f * rng)} ); })} {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 ( {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 ( setHovered({ lineKey: l.key, ptIdx: i })} onMouseLeave={() => setHovered(null)} style={{ cursor: "pointer" }} > {isHov && ( {fmt(p[l.key])} )} ); })} ); })} {pts.map((p, i) => ( {fmtDatum(p.datum)} ))}
{lines.map((l) => (
{l.label}
))}
); } // ── 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 (
setDatum(e.target.value)} style={{ ...is, width: "100%", marginBottom: 16 }} />
€ setEv(e.target.value)} placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
€ setSchuld(e.target.value)} placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
setOpmerkingen(e.target.value)} placeholder="Optionele notitie..." style={{ ...is, width: "100%" }} /> {err && (
{err}
)}
); } function VLabel({ label, T }) { return (
{label}
); } // ── 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 (

Voortgang Schuld & EV

{/* Hero netto */}
Mijn Nettowaarde
= 0 ? GREEN : RED }}>{fmt(nettoWaarde)}
Eigen Vermogen
{fmt(totEV)}
Totale Schuld
{fmt(totSchuld)}
{/* KPI kaarten */}
{/* Schuld verdeling */}
Verdeling Schuld
{schuldCatsDash.map((b) => (
{b.naam}
{fmt(b.subtot)}
))}
Totaal schuld {fmt(totSchuld)}
{/* EV verdeling */}
Verdeling Eigen Vermogen
{catTotals.map((c) => (
{c.naam}
{fmt(c.tot)}
))}
Totaal EV {fmt(totEV)}
{/* Rendement */}
Rendement Eigen Vermogen
Bedrag
—
Percentage
—
{/* Donut */}
Eigen Vermogen
Schuld
{/* Grafieken */}
Ontwikkeling Schuld en EV
Ontwikkeling Mijn Nettowaarde
{/* Voortgang tabel */}
Voortgang overzicht
{!locked && ( )}
{showAdd && !locked && ( setShowAdd(false)} T={T} darkMode={darkMode} /> )} {editRow && !locked && ( setEditRow(null)} T={T} darkMode={darkMode} initialData={editRow} /> )} {voortgangRows.length > 0 ? (
{kolommen.map((k) => )} {!locked && } {voortgangRows.map((row, i) => ( {/* title: bij een smalle kolom is de tekst afgekapt */} {!locked && ( )} ))}
Datum Eigen Vermogen Schuld Nettowaarde Opmerkingen
{fmtDatum(row.datum)} {row.ev !== null ? fmt(row.ev) : "—"} {row.schuld !== null ? fmt(row.schuld) : "—"} = 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}{row.opmerkingen || "—"}
) : (
Nog geen gecombineerde datapunten. Voeg schuld- en EV-momenten toe.
)}
); }