Files
VibeFinance/frontend/src/pages/EigenVermogenTab.jsx
T

654 lines
30 KiB
React
Raw Normal View History

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 (
<Overlay onClose={onClose}>
<ModalBox>
<ModalHeader
icon="✏️" iconBg={cat.color}
title="Aanpassen" subtitle={cat.naam} subtitleColor={cat.color}
onClose={onClose}
headerBg={darkMode ? `${cat.color}11` : `${cat.color}09`}
T={T}
/>
<div style={{ padding: "20px 22px", overflowY: "auto", maxHeight: "65vh" }}>
{/* Naam + emoji */}
<FieldLabel label="Categorie naam" T={T} />
<div style={{ marginBottom: 16 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<button onClick={() => setShowEmoji((p) => !p)} style={{
width: 38, height: 38, borderRadius: 8,
background: `${cat.color}22`,
border: `2px solid ${showEmoji ? cat.color : cat.color + "44"}`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 20, flexShrink: 0, cursor: "pointer",
}}>
<AppIcon value={catIcon} fallback="🏷" size={20} />
</button>
<input value={catNaam} onChange={(e) => setCatNaam(e.target.value)}
style={{ ...is, flex: 1, borderLeft: `3px solid ${cat.color}` }}
placeholder="Naam categorie" />
</div>
{showEmoji && (
<EmojiPicker
selected={catIcon} T={T} accentColor={cat.color}
onSelect={(e) => { setCatIcon(e); setShowEmoji(false); }}
onClear={() => { setCatIcon(""); setShowEmoji(false); }}
/>
)}
</div>
{/* Kleur */}
<FieldLabel label="Kleur" T={T} />
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 16 }}>
{CAT_COLORS.map((c) => (
<button key={c} onClick={() => setCatColor(c)} style={{
width: 28, height: 28, borderRadius: "50%", background: c,
border: `3px solid ${catColor === c ? "#fff" : "transparent"}`,
cursor: "pointer", outline: catColor === c ? `2px solid ${c}` : "none", outlineOffset: 1,
}} />
))}
</div>
{/* Bestaande items */}
<FieldLabel label="Items bewerken" T={T} />
{items.map((item) => (
<div key={item.id} style={{
display: "flex", alignItems: "center", gap: 8, marginBottom: 10,
background: darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc",
border: `1px solid ${T.border}`, borderLeft: `3px solid ${cat.color}`,
borderRadius: 10, padding: "10px 12px",
}}>
<input value={item.naam} onChange={(e) => updateItem(item.id, "naam", e.target.value)}
style={{ ...is, flex: 1 }} placeholder="Naam" />
<EuroInput value={item.waarde} onChange={(v) => updateItem(item.id, "waarde", v)} is={is} T={T} />
{items.length > 1 && (
<button onClick={() => removeItemLocal(item.id)} style={deleteSmBtn}>×</button>
)}
</div>
))}
{/* Nieuw item */}
<div style={{ marginTop: 18, paddingTop: 16, borderTop: `1px solid ${T.border}` }}>
<FieldLabel label="Nieuw item toevoegen" T={T} />
<div style={{
display: "flex", alignItems: "center", gap: 8,
background: darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc",
border: `1px solid ${T.border}`, borderLeft: `3px solid ${PURPLE}`,
borderRadius: 10, padding: "10px 12px",
}}>
<input value={nieuwNaam}
onChange={(e) => { setNieuwNaam(e.target.value); setErr(""); }}
placeholder="Naam" style={{ ...is, flex: 1 }} />
<EuroInput value={nieuwVal} onChange={setNieuwVal} is={is} T={T} />
<button onClick={addNieuw} style={addBtn}>+ Voeg toe</button>
</div>
{err && <ErrMsg msg={err} />}
</div>
</div>
<ModalFooter T={T}>
{confirmDel ? (
<ConfirmDeleteDialog
naam={cat.naam} T={T}
onCancel={() => setConfirmDel(false)}
onConfirm={handleDelete}
/>
) : (
<>
<button onClick={() => setConfirmDel(true)} style={trashBtn}>🗑 Verwijderen</button>
<button onClick={onClose} style={cancelBtn(T)}>Annuleren</button>
<button onClick={handleSave} style={{
flex: 2, padding: "10px",
background: `linear-gradient(135deg, ${cat.color}, ${cat.color}cc)`,
border: "none", borderRadius: 10, color: "#fff",
fontWeight: 700, cursor: "pointer", fontSize: 13,
}}>Opslaan</button>
</>
)}
</ModalFooter>
</ModalBox>
</Overlay>
);
}
// ── CatKaart ──────────────────────────────────────────────────────────────────
function CatKaart({ cat }) {
const { locked, T } = useApp();
const [showAanpassen, setShowAanpassen] = useState(false);
return (
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderTop: `3px solid ${cat.color}` }}>
{showAanpassen && <AanpassenPopup cat={cat} onClose={() => setShowAanpassen(false)} />}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<span style={{ fontWeight: 700, fontSize: 14, color: cat.color }}>
{cat.icon && <span style={{ marginRight: 6 }}><AppIcon value={cat.icon} size={16} /></span>}
{cat.naam}
</span>
{!locked && (
<button onClick={() => setShowAanpassen(true)} style={{
padding: "4px 10px", background: `${cat.color}22`,
border: `1px solid ${cat.color}44`, borderRadius: 7,
color: cat.color, cursor: "pointer", fontSize: 11, fontWeight: 700,
}}>Aanpassen</button>
)}
</div>
{cat.items.map((item) => (
<div key={item.id} style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 8 }}>
<span style={{ fontSize: 13, color: T.muted, flex: 1, minWidth: 0, wordBreak: "break-word" }}>
{item.naam}
</span>
<span style={{ fontSize: 14, fontWeight: 700, color: T.text, whiteSpace: "nowrap" }}>
{fmt(item.waarde)}
</span>
</div>
))}
<div style={{
marginTop: 10, paddingTop: 10,
borderTop: `1px solid ${T.border}`,
display: "flex", justifyContent: "flex-end",
}}>
<span style={{ fontSize: 14, fontWeight: 700, color: cat.color }}>
{fmt(cat.items.reduce((a, i) => a + (i.waarde || 0), 0))}
</span>
</div>
</VCard>
);
}
// ── 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 (
<div>
{/* Modal */}
{showModal && (
<Overlay onClose={closeModal}>
<ModalBox maxWidth={560}>
<ModalHeader
icon="📊" iconBg={PURPLE}
title={editDatum ? "Rij bewerken" : "Nieuw geïnvesteerde fiat"}
subtitle="Vul de waarden in voor dit moment"
subtitleColor={T.muted}
onClose={closeModal}
headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"}
T={T}
/>
<div style={{ padding: "20px 22px", overflowY: "auto", maxHeight: "70vh" }}>
<FieldLabel label="Datum *" T={T} />
<input type="date" value={input.datum}
onChange={(e) => setInput((s) => ({ ...s, datum: e.target.value }))}
style={{ ...is, width: "100%", marginBottom: 16 }} />
<FieldLabel label="Bedragen per categorie" T={T} />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 16 }}>
{cats.map((c) => (
<div key={c.id} style={{
display: "flex", alignItems: "center", gap: 12,
background: darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc",
border: `1px solid ${T.border}`, borderLeft: `3px solid ${c.color}`,
borderRadius: 10, padding: "8px 12px",
}}>
<span style={{ fontSize: 12, color: c.color, fontWeight: 700, minWidth: 90, flexShrink: 0 }}>{c.naam}</span>
<EuroInput
value={input[c.id]}
onChange={(v) => setInput((s) => ({ ...s, [c.id]: v }))}
is={is} T={T}
/>
</div>
))}
</div>
<FieldLabel label="Netto Waarde *" T={T} />
<EuroInput
value={input.nettoWaarde}
onChange={(v) => setInput((s) => ({ ...s, nettoWaarde: v }))}
is={is} T={T}
/>
<div style={{ marginBottom: 16 }} />
<FieldLabel label="Opmerkingen" T={T} />
<input type="text" value={input.opmerkingen}
onChange={(e) => setInput((s) => ({ ...s, opmerkingen: e.target.value }))}
placeholder="Optionele notitie..." style={{ ...is, width: "100%" }} />
{err && <ErrMsg msg={err} />}
</div>
<ModalFooter T={T}>
<button onClick={closeModal} style={cancelBtn(T)}>Annuleren</button>
<button onClick={addRow} style={primaryBtn}>{editDatum ? "Opslaan" : "Toevoegen"}</button>
</ModalFooter>
</ModalBox>
</Overlay>
)}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
<div style={{ fontWeight: 700, fontSize: 14, color: T.text }}>Geïnvesteerde Fiat</div>
{!locked && (
<button onClick={openModal} style={primaryBtn}>+ Toevoegen</button>
)}
</div>
{evRows.length > 0 ? (
<div style={{ overflowX: "auto" }}>
<div style={{ maxHeight: 340, overflowY: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 600 }}>
<thead>
<tr>
<th style={th}>Datum</th>
{cats.map((c) => <th key={c.id} style={{ ...th, color: c.color }}>{c.naam}</th>)}
<th style={th}>Netto Waarde</th>
<th style={th}>Verschil</th>
<th style={th}>Opmerkingen</th>
{!locked && <th style={th}></th>}
</tr>
</thead>
<tbody>
{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 (
<tr key={i}>
<td style={td}>{fmtDatum(row.datum)}</td>
{cats.map((c) => <td key={c.id} style={{ ...td, color: c.color }}>{fmt(row[c.id] || 0)}</td>)}
<td style={{ ...td, fontWeight: 700, color: PURPLE_LIGHT }}>{fmt(row.nettoWaarde || 0)}</td>
<td style={{ ...td, fontWeight: 700, color: vColor }}>{vLabel}</td>
<td style={{ ...td, color: T.muted }}>{row.opmerkingen || "—"}</td>
{!locked && (
<td style={td}>
<div style={{ display: "flex", gap: 6 }}>
<button onClick={() => openEdit(row)} style={editBtn}>✏️</button>
<button onClick={() => removeRow(row.datum)} style={deleteSmBtn}>×</button>
</div>
</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : (
<EmptyState
icon="📊" text="Nog geen datapunten"
hint="Klik hier om je eerste datapunt toe te voegen"
locked={locked} onClick={openModal} T={T}
/>
)}
</div>
);
}
// ── 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 (
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 20 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 14 }}>🎯</span>
<span style={{ fontWeight: 600, fontSize: 14 }}>Gewenst doel EV</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<span style={{ fontWeight: 700, fontSize: 15, color: PURPLE_LIGHT }}>{fmt(totVerdeling)}</span>
{editing ? (
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ color: T.muted, fontSize: 13 }}>/</span>
<input
autoFocus type="number" value={val}
onChange={(e) => 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",
}}
/>
<button onClick={save} style={{ padding: "4px 10px", background: PURPLE, border: "none", borderRadius: 7, color: "#fff", cursor: "pointer", fontSize: 12, fontWeight: 700 }}>OK</button>
</div>
) : (
<>
<span style={{ color: T.muted, fontSize: 13 }}>/ {fmt(doel)}</span>
{!locked && (
<button onClick={openEdit} style={{ background: "transparent", border: "none", cursor: "pointer", color: T.muted, fontSize: 14, padding: 2 }}>✏️</button>
)}
</>
)}
<span style={{ color: PURPLE_LIGHT, fontWeight: 700, fontSize: 13 }}>{Math.round(progressPct * 100)}%</span>
</div>
</div>
<div style={{ height: 12, background: "rgba(255,255,255,0.07)", borderRadius: 99, overflow: "hidden" }}>
<div style={{
height: "100%", width: `${progressPct * 100}%`,
background: `linear-gradient(90deg,${PURPLE},#a855f7)`,
borderRadius: 99, transition: "width 0.7s ease",
}} />
</div>
</VCard>
);
}
// ── 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 (
<div>
<h1 style={{ fontSize: 20, fontWeight: 800, margin: "0 0 20px 0" }}>Eigen Vermogen</h1>
{/* Doel progressie */}
<DoelProgressie ev={ev} data={data} setData={setData} totVerdeling={totVerdeling} locked={locked} T={T} />
{/* Summary blokken */}
<div style={{ display: "flex", gap: 16, marginBottom: 20, flexWrap: "wrap" }}>
{summaryBlocks.map((block) => (
<div key={block.title} style={{
background: T.card, border: `1px solid ${T.border}`,
borderTop: `3px solid ${block.accent}`, borderRadius: 12,
padding: "16px 18px", flex: 1, minWidth: 200,
}}>
<div style={{
fontWeight: 700, fontSize: 12, letterSpacing: "0.1em",
textTransform: "uppercase", color: block.accent, marginBottom: 12,
}}>{block.title}</div>
{block.rows.map(({ label, value, color }) => (
<div key={label} style={{
display: "flex", justifyContent: "space-between",
padding: "6px 0", borderBottom: `1px solid ${T.border}`,
}}>
<span style={{ fontSize: 13, color: T.muted }}>{label}</span>
<span style={{ fontSize: 13, fontWeight: 600, color }}>{value}</span>
</div>
))}
<div style={{
display: "flex", justifyContent: "space-between",
alignItems: "center", marginTop: 10, paddingTop: 8,
}}>
<span style={{ fontSize: 12, fontWeight: 700, color: T.text }}>Totaal</span>
<span style={{ fontSize: 15, fontWeight: 800, color: block.totalColor }}>{block.total}</span>
</div>
</div>
))}
{totVerdeling > 0 && (
<div style={{
background: T.card, border: `1px solid ${T.border}`,
borderRadius: 16, padding: "20px",
flex: 1, minWidth: 200,
display: "flex", flexDirection: "column", alignItems: "center",
}}>
<DonutChart segments={catTotals.map((c) => ({ val: c.tot, color: c.color, naam: c.naam }))} total={totVerdeling} size={200} label="Verdeling" centerVal={fmt(totVerdeling)} />
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px 14px", marginTop: 12, justifyContent: "center" }}>
{catTotals.map((c) => (
<div key={c.id} style={{ display: "flex", alignItems: "center", gap: 5 }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: c.color }} />
<span style={{ fontSize: 11, color: T.subtext }}>{c.naam}</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Nieuwe categorie */}
{!locked && (
<div style={{ marginBottom: 16 }}>
<button onClick={() => setShowAddCat(true)} style={{
padding: "10px 20px", background: `${PURPLE}22`,
border: `1px solid ${PURPLE}44`, borderRadius: 10,
color: PURPLE_LIGHT, cursor: "pointer", fontWeight: 700, fontSize: 13,
}}>+ Nieuwe categorie</button>
</div>
)}
{showAddCat && (
<Overlay onClose={() => setShowAddCat(false)}>
<ModalBox maxWidth={400}>
<ModalHeader icon="➕" iconBg={PURPLE} title="Nieuwe categorie" subtitle="Voeg een EV-categorie toe" subtitleColor={PURPLE_LIGHT} onClose={() => setShowAddCat(false)} headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"} T={T} />
<div style={{ padding: "20px 22px" }}>
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", marginBottom: 6 }}>Naam *</div>
<input autoFocus placeholder="Bijv. Aandelen" value={newNaam}
onChange={(e) => setNewNaam(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addCat()}
style={{ ...iStyle2, marginBottom: 16, width: "100%", boxSizing: "border-box" }} />
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", marginBottom: 8 }}>Kleur</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{CAT_COLORS.map((c) => (
<button key={c} onClick={() => setNewColor(c)} style={{
width: 28, height: 28, borderRadius: "50%", background: c,
border: `3px solid ${newColor === c ? "#fff" : "transparent"}`,
cursor: "pointer", outline: newColor === c ? `2px solid ${c}` : "none", outlineOffset: 1,
}} />
))}
</div>
</div>
<ModalFooter T={T}>
<button onClick={() => setShowAddCat(false)} style={{ flex: 1, padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 }}>Annuleren</button>
<button onClick={addCat} style={{ flex: 2, padding: "10px", background: `linear-gradient(135deg, ${PURPLE}, #a855f7)`, border: "none", borderRadius: 10, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 }}>Aanmaken</button>
</ModalFooter>
</ModalBox>
</Overlay>
)}
{/* Categorie kaarten */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16, marginBottom: 20,
}}>
{cats.map((cat) => <CatKaart key={cat.id} cat={cat} />)}
</div>
{/* Voortgang tabel */}
<VCard style={{ background: T.card, border: `1px solid ${T.border}` }}>
<EVVoortgangTabel />
</VCard>
</div>
);
}
// ── 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 (
<div style={{
fontSize: 11, color: T.muted, fontWeight: 700,
textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 8,
}}>{label}</div>
);
}
function EuroInput({ value, onChange, is, T }) {
return (
<div style={{ position: "relative", width: 110, flexShrink: 0 }}>
<span style={{
position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)",
fontSize: 12, color: T.muted, pointerEvents: "none",
}}>€</span>
<input type="number" value={value} onChange={(e) => onChange(e.target.value)}
placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} />
</div>
);
}
function ErrMsg({ msg }) {
return (
<div style={{
color: RED, fontSize: 12, marginTop: 8,
padding: "6px 10px", background: "rgba(239,68,68,0.1)",
borderRadius: 8, border: "1px solid rgba(239,68,68,0.25)",
}}>{msg}</div>
);
}
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 });