import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js";
import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js";
import {
VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter,
EmojiPicker, AppIcon, ConfirmDeleteDialog, EmptyState,
} from "../components/ui/index.jsx";
// ── AanpassenPopup (EV categorie) ─────────────────────────────────────────────
function AanpassenPopup({ cat, onClose }) {
const { data, setData, ev, cats, darkMode, T } = useApp();
const [items, setItems] = useState(cat.items.map((i) => ({ ...i })));
const [catNaam, setCatNaam] = useState(cat.naam);
const [catColor, setCatColor] = useState(cat.color);
const [catIcon, setCatIcon] = useState(cat.icon || "");
const [showEmoji, setShowEmoji] = useState(false);
const [nieuwNaam, setNieuwNaam] = useState("");
const [nieuwVal, setNieuwVal] = useState("");
const [err, setErr] = useState("");
const [confirmDel, setConfirmDel] = useState(false);
const is = inputStyle(T);
const updateItem = (id, field, val) =>
setItems((p) => p.map((i) => i.id !== id ? i : { ...i, [field]: val }));
const removeItemLocal = (id) => {
if (items.length <= 1) return;
setItems((p) => p.filter((i) => i.id !== id));
};
const addNieuw = () => {
if (!nieuwNaam.trim()) { setErr("Naam is verplicht."); return; }
setItems((p) => [...p, { id: `i${Date.now()}`, naam: nieuwNaam.trim(), waarde: parseFloat(nieuwVal) || 0 }]);
setNieuwNaam(""); setNieuwVal(""); setErr("");
};
const handleSave = () => {
const updated = {
...data,
eigenVermogen: {
...ev,
categories: cats.map((c) =>
c.id !== cat.id ? c : {
...c,
naam: catNaam.trim() || c.naam,
color: catColor,
icon: catIcon,
items: items.map((i) => ({ ...i, waarde: parseFloat(i.waarde) || 0 })),
}
),
},
};
setData(updated); onClose();
};
const handleDelete = () => {
setData({ ...data, eigenVermogen: { ...ev, categories: cats.filter((c) => c.id !== cat.id) } });
onClose();
};
return (
{/* Naam + emoji */}
setCatNaam(e.target.value)}
style={{ ...is, flex: 1, borderLeft: `3px solid ${cat.color}` }}
placeholder="Naam categorie" />
{showEmoji && (
{ setCatIcon(e); setShowEmoji(false); }}
onClear={() => { setCatIcon(""); setShowEmoji(false); }}
/>
)}
{/* Kleur */}
{CAT_COLORS.map((c) => (
{/* Bestaande items */}
{items.map((item) => (
updateItem(item.id, "naam", e.target.value)}
style={{ ...is, flex: 1 }} placeholder="Naam" />
updateItem(item.id, "waarde", v)} is={is} T={T} />
{items.length > 1 && (
)}
))}
{/* Nieuw item */}
{ setNieuwNaam(e.target.value); setErr(""); }}
placeholder="Naam" style={{ ...is, flex: 1 }} />
{err &&
}
{confirmDel ? (
setConfirmDel(false)}
onConfirm={handleDelete}
/>
) : (
<>
>
)}
);
}
// ── CatKaart ──────────────────────────────────────────────────────────────────
function CatKaart({ cat }) {
const { locked, T } = useApp();
const [showAanpassen, setShowAanpassen] = useState(false);
return (
{showAanpassen && setShowAanpassen(false)} />}
{cat.icon && }
{cat.naam}
{!locked && (
)}
{cat.items.map((item) => (
{item.naam}
{fmt(item.waarde)}
))}
{fmt(cat.items.reduce((a, i) => a + (i.waarde || 0), 0))}
);
}
// ── EVVoortgangTabel ──────────────────────────────────────────────────────────
function EVVoortgangTabel() {
const { evRows, setEvRows, cats, locked, darkMode, T } = useApp();
const empty = () => ({
datum: "", nettoWaarde: "", opmerkingen: "",
...cats.reduce((a, c) => ({ ...a, [c.id]: "" }), {}),
});
const [showModal, setShowModal] = useState(false);
const [input, setInput] = useState(empty);
const [editDatum, setEditDatum] = useState(null);
const [err, setErr] = useState("");
const is = inputStyle(T);
const openModal = () => { setInput(empty()); setEditDatum(null); setErr(""); setShowModal(true); };
const openEdit = (row) => {
setInput({ ...row, opmerkingen: row.opmerkingen || "" });
setEditDatum(row.datum); setErr(""); setShowModal(true);
};
const closeModal = () => { setShowModal(false); setErr(""); setEditDatum(null); };
const addRow = () => {
if (!input.datum) { setErr("Datum is verplicht."); return; }
const parsedCats = cats.reduce((a, c) => ({ ...a, [c.id]: parseFloat(input[c.id]) || 0 }), {});
const hasValue = cats.some((c) => parsedCats[c.id] > 0);
if (!hasValue) { setErr("Vul minimaal één bedrag in."); return; }
const row = {
...input,
...parsedCats,
nettoWaarde: parseFloat(input.nettoWaarde) || 0,
};
const updated = [...evRows.filter((r) => r.datum !== row.datum), row]
.sort((a, b) => b.datum.localeCompare(a.datum));
setEvRows(updated); closeModal();
};
const removeRow = (datum) => setEvRows(evRows.filter((r) => r.datum !== datum));
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", background: T.card, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` };
return (
{/* Modal */}
{showModal && (
)}
Geïnvesteerde Fiat
{!locked && (
)}
{evRows.length > 0 ? (
| Datum |
{cats.map((c) => {c.naam} | )}
Netto Waarde |
Verschil |
Opmerkingen |
{!locked && | }
{evRows.map((row, i) => {
const vorigeNW = i < evRows.length - 1 ? (evRows[i + 1].nettoWaarde || 0) : null;
const verschil = vorigeNW !== null ? (row.nettoWaarde || 0) - vorigeNW : null;
const vColor = verschil === null ? T.muted : verschil > 0 ? GREEN : verschil < 0 ? RED : T.muted;
const vLabel = verschil === null ? "—" : `${verschil >= 0 ? "+" : ""}${fmt(verschil)}`;
return (
| {fmtDatum(row.datum)} |
{cats.map((c) => {fmt(row[c.id] || 0)} | )}
{fmt(row.nettoWaarde || 0)} |
{vLabel} |
{row.opmerkingen || "—"} |
{!locked && (
|
)}
);
})}
) : (
)}
);
}
// ── DoelProgressie ────────────────────────────────────────────────────────────
function DoelProgressie({ ev, data, setData, totVerdeling, locked, T }) {
const [editing, setEditing] = useState(false);
const [val, setVal] = useState("");
const doel = ev.doel || 1;
const progressPct = Math.min(totVerdeling / doel, 1);
const openEdit = () => { setVal(String(doel)); setEditing(true); };
const save = () => {
const n = parseFloat(val);
if (n > 0) setData({ ...data, eigenVermogen: { ...ev, doel: n } });
setEditing(false);
};
return (
🎯
Gewenst doel EV
{fmt(totVerdeling)}
{editing ? (
/
setVal(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }}
style={{
width: 130, padding: "4px 8px", borderRadius: 8, fontSize: 13,
background: T.inputBg, border: `1px solid ${PURPLE}`, color: T.text, outline: "none",
}}
/>
) : (
<>
/ {fmt(doel)}
{!locked && (
)}
>
)}
{Math.round(progressPct * 100)}%
);
}
// ── EigenVermogenTab (pagina) ─────────────────────────────────────────────────
export default function EigenVermogenTab() {
const { cats, catTotals, ev, data, setData, locked, darkMode, T, evRows } = useApp();
const [showAddCat, setShowAddCat] = useState(false);
const [newNaam, setNewNaam] = useState("");
const [newColor, setNewColor] = useState(CAT_COLORS[0]);
const totVerdeling = catTotals.reduce((a, c) => a + c.tot, 0);
const sumCat = (id) => evRows.reduce((a, r) => a + (r[id] || 0), 0);
const totGeinv = cats.reduce((a, c) => a + sumCat(c.id), 0);
const iStyle2 = inputStyle(T);
const addCat = () => {
if (!newNaam.trim()) return;
setData({
...data,
eigenVermogen: {
...ev,
categories: [...cats, {
id: `c${Date.now()}`, naam: newNaam, color: newColor,
items: [{ id: `i${Date.now()}`, naam: "Item 1", waarde: 0 }],
}],
},
});
setNewNaam(""); setShowAddCat(false);
};
const summaryBlocks = [
{
title: "Verdeling Eigen Vermogen", accent: GREEN,
rows: catTotals.map((c) => ({ label: c.naam, value: fmt(c.tot), color: c.color })),
total: fmt(totVerdeling), totalColor: GREEN,
},
{
title: "Geïnvesteerde Fiat", accent: PURPLE_LIGHT,
rows: cats.map((c) => ({ label: c.naam, value: evRows.length > 0 ? fmt(sumCat(c.id)) : "—", color: c.color })),
total: evRows.length > 0 ? fmt(totGeinv) : "—", totalColor: PURPLE_LIGHT,
},
{
title: "Rendement", accent: "#f59e0b",
rows: cats.map((c) => {
const ev2 = catTotals.find((ct) => ct.id === c.id)?.tot ?? 0;
const g = sumCat(c.id);
const r = g > 0 ? (ev2 - g) / g : null;
return { label: c.naam, value: r === null ? "—" : `${(r * 100).toFixed(1)}%`, color: r === null ? T.muted : r >= 0 ? GREEN : RED };
}),
total: (() => {
const et = totVerdeling;
const gt = totGeinv;
return gt > 0 ? `${(((et - gt) / gt) * 100).toFixed(1)}%` : "—";
})(),
totalColor: (() => {
return totGeinv > 0 ? (totVerdeling >= totGeinv ? GREEN : RED) : T.muted;
})(),
},
];
return (
Eigen Vermogen
{/* Doel progressie */}
{/* Summary blokken */}
{summaryBlocks.map((block) => (
{block.title}
{block.rows.map(({ label, value, color }) => (
{label}
{value}
))}
Totaal
{block.total}
))}
{totVerdeling > 0 && (
({ val: c.tot, color: c.color, naam: c.naam }))} total={totVerdeling} size={200} label="Verdeling" centerVal={fmt(totVerdeling)} />
{catTotals.map((c) => (
))}
)}
{/* Nieuwe categorie */}
{!locked && (
)}
{showAddCat && (
setShowAddCat(false)}>
setShowAddCat(false)} headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"} T={T} />
Naam *
setNewNaam(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addCat()}
style={{ ...iStyle2, marginBottom: 16, width: "100%", boxSizing: "border-box" }} />
Kleur
{CAT_COLORS.map((c) => (
)}
{/* Categorie kaarten */}
{cats.map((cat) => )}
{/* Voortgang tabel */}
);
}
// ── Gedeelde kleine helpers ────────────────────────────────────────────────────
function inputStyle(T) {
return {
background: T.inputBg, border: `1px solid ${T.inputBorder}`,
borderRadius: 8, color: T.text, padding: "8px 10px",
fontSize: 13, outline: "none", boxSizing: "border-box",
};
}
function FieldLabel({ label, T }) {
return (
{label}
);
}
function EuroInput({ value, onChange, is, T }) {
return (
€
onChange(e.target.value)}
placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
);
}
function ErrMsg({ msg }) {
return (
{msg}
);
}
const primaryBtn = { padding: "10px 20px", background: PURPLE, border: "none", borderRadius: 8, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 };
const editBtn = { padding: "2px 10px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 600 };
const deleteSmBtn = { 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 };
const trashBtn = { padding: "10px 14px", background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 10, color: RED, cursor: "pointer", fontWeight: 600, fontSize: 13 };
const addBtn = { padding: "6px 14px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 8, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 };
const cancelBtn = (T) => ({ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 });