456 lines
22 KiB
React
456 lines
22 KiB
React
|
|
import { useState } from "react";
|
|||
|
|
import { useApp } from "../context/AppContext.jsx";
|
|||
|
|
import { fmt, pct } from "../utils/format.js";
|
|||
|
|
import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
|
|||
|
|
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, EmojiPicker, AppIcon, ConfirmDeleteDialog } from "../components/ui/index.jsx";
|
|||
|
|
|
|||
|
|
// ── SchuldAanpassenPopup ──────────────────────────────────────────────────────
|
|||
|
|
function SchuldAanpassenPopup({ blok, onClose }) {
|
|||
|
|
const { data, setData, schuld, schuldBlokken, darkMode, T } = useApp();
|
|||
|
|
const [items, setItems] = useState(blok.items.map((i) => ({ ...i })));
|
|||
|
|
const [blokNaam, setBlokNaam] = useState(blok.naam);
|
|||
|
|
const [blokIcon, setBlokIcon] = useState(blok.icon || "");
|
|||
|
|
const [showEmoji, setShowEmoji] = useState(false);
|
|||
|
|
const [nieuwNaam, setNieuwNaam] = useState("");
|
|||
|
|
const [nieuwVal, setNieuwVal] = useState("");
|
|||
|
|
const [nieuwErr, setNieuwErr] = 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()) { setNieuwErr("Naam is verplicht."); setTimeout(() => setNieuwErr(""), 3000); return; }
|
|||
|
|
setItems((p) => [...p, { id: `si${Date.now()}`, naam: nieuwNaam.trim(), waarde: parseFloat(nieuwVal) || 0 }]);
|
|||
|
|
setNieuwNaam(""); setNieuwVal(""); setNieuwErr("");
|
|||
|
|
};
|
|||
|
|
const handleSave = () => {
|
|||
|
|
setData({
|
|||
|
|
...data,
|
|||
|
|
schuld: {
|
|||
|
|
...schuld,
|
|||
|
|
blokken: schuldBlokken.map((b) =>
|
|||
|
|
b.id !== blok.id ? b : {
|
|||
|
|
...b, naam: blokNaam.trim() || b.naam, icon: blokIcon,
|
|||
|
|
items: items.map((i) => ({ ...i, waarde: parseFloat(i.waarde) || 0 })),
|
|||
|
|
}
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
onClose();
|
|||
|
|
};
|
|||
|
|
const handleDelete = () => {
|
|||
|
|
setData({ ...data, schuld: { ...schuld, blokken: schuldBlokken.filter((b) => b.id !== blok.id) } });
|
|||
|
|
onClose();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Overlay onClose={onClose}>
|
|||
|
|
<ModalBox>
|
|||
|
|
<ModalHeader
|
|||
|
|
icon="✏️" iconBg={blok.color}
|
|||
|
|
title="Aanpassen" subtitle={blok.naam} subtitleColor={blok.color}
|
|||
|
|
onClose={onClose}
|
|||
|
|
headerBg={darkMode ? `${blok.color}11` : `${blok.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: `${blok.color}22`,
|
|||
|
|
border: `2px solid ${showEmoji ? blok.color : blok.color + "44"}`,
|
|||
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|||
|
|
fontSize: 20, flexShrink: 0, cursor: "pointer",
|
|||
|
|
}}><AppIcon value={blokIcon} fallback="🏷" size={20} /></button>
|
|||
|
|
<input value={blokNaam} onChange={(e) => setBlokNaam(e.target.value)}
|
|||
|
|
style={{ ...is, flex: 1, borderLeft: `3px solid ${blok.color}` }}
|
|||
|
|
placeholder="Naam categorie" />
|
|||
|
|
</div>
|
|||
|
|
{showEmoji && (
|
|||
|
|
<EmojiPicker
|
|||
|
|
selected={blokIcon} T={T} accentColor={blok.color}
|
|||
|
|
onSelect={(e) => { setBlokIcon(e); setShowEmoji(false); }}
|
|||
|
|
onClear={() => { setBlokIcon(""); setShowEmoji(false); }}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 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 ${blok.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); setNieuwErr(""); }}
|
|||
|
|
placeholder="Naam" style={{ ...is, flex: 1 }} />
|
|||
|
|
<EuroInput value={nieuwVal} onChange={setNieuwVal} is={is} T={T} />
|
|||
|
|
<button onClick={addNieuw} style={addBtn}>+ Voeg toe</button>
|
|||
|
|
</div>
|
|||
|
|
{nieuwErr && <ErrMsg msg={nieuwErr} />}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Footer */}
|
|||
|
|
<div style={{ padding: "14px 22px", borderTop: `1px solid ${T.border}` }}>
|
|||
|
|
{confirmDel ? (
|
|||
|
|
<ConfirmDeleteDialog
|
|||
|
|
naam={blok.naam} T={T}
|
|||
|
|
onCancel={() => setConfirmDel(false)}
|
|||
|
|
onConfirm={handleDelete}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div style={{ display: "flex", gap: 10 }}>
|
|||
|
|
<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, ${blok.color}, ${blok.color}cc)`,
|
|||
|
|
border: "none", borderRadius: 10, color: "#fff",
|
|||
|
|
fontWeight: 700, cursor: "pointer", fontSize: 13,
|
|||
|
|
}}>Opslaan</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</ModalBox>
|
|||
|
|
</Overlay>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── SchuldKaart ───────────────────────────────────────────────────────────────
|
|||
|
|
function SchuldKaart({ blok }) {
|
|||
|
|
const { locked, T } = useApp();
|
|||
|
|
const [showAanpassen, setShowAanpassen] = useState(false);
|
|||
|
|
const subtot = blok.items.reduce((a, i) => a + (i.waarde || 0), 0);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, borderTop: `3px solid ${blok.color}` }}>
|
|||
|
|
{showAanpassen && <SchuldAanpassenPopup blok={blok} onClose={() => setShowAanpassen(false)} />}
|
|||
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
|
|||
|
|
<span style={{ fontWeight: 700, fontSize: 14, color: blok.color }}>
|
|||
|
|
{blok.icon && <span style={{ marginRight: 6 }}><AppIcon value={blok.icon} size={16} /></span>}
|
|||
|
|
{blok.naam}
|
|||
|
|
</span>
|
|||
|
|
{!locked && (
|
|||
|
|
<button onClick={() => setShowAanpassen(true)} style={{
|
|||
|
|
padding: "4px 10px", background: `${blok.color}22`,
|
|||
|
|
border: `1px solid ${blok.color}44`, borderRadius: 7,
|
|||
|
|
color: blok.color, cursor: "pointer", fontSize: 11, fontWeight: 700,
|
|||
|
|
}}>Aanpassen</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{blok.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: 13, fontWeight: 600, color: blok.color, whiteSpace: "nowrap" }}>
|
|||
|
|
{fmt(item.waarde)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
<div style={{
|
|||
|
|
marginTop: 8, paddingTop: 8,
|
|||
|
|
borderTop: `1px solid ${T.border}`,
|
|||
|
|
display: "flex", justifyContent: "flex-end",
|
|||
|
|
}}>
|
|||
|
|
<span style={{ fontSize: 14, fontWeight: 700, color: blok.color }}>{fmt(subtot)}</span>
|
|||
|
|
</div>
|
|||
|
|
</VCard>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── AflossingProgressie ───────────────────────────────────────────────────────
|
|||
|
|
function AflossingProgressie() {
|
|||
|
|
const { data, setData, schuld, totSchuld, locked, darkMode, T } = useApp();
|
|||
|
|
const init = schuld.initieleSchuld || 0;
|
|||
|
|
const betaald = Math.min(Math.max(init - totSchuld, 0), init);
|
|||
|
|
const aflossingPct = init > 0 ? betaald / init : 0;
|
|||
|
|
const [editOpen, setEditOpen] = useState(false);
|
|||
|
|
const [editVal, setEditVal] = useState(schuld.initieleSchuld || "");
|
|||
|
|
|
|||
|
|
const saveInitiele = () => {
|
|||
|
|
setData({ ...data, schuld: { ...schuld, initieleSchuld: parseFloat(editVal) || 0 } });
|
|||
|
|
setEditOpen(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginTop: 4 }}>
|
|||
|
|
{editOpen && (
|
|||
|
|
<Overlay onClose={() => setEditOpen(false)}>
|
|||
|
|
<ModalBox maxWidth={380}>
|
|||
|
|
<ModalHeader
|
|||
|
|
icon="✏️" iconBg={RED}
|
|||
|
|
title="Initiële schuld aanpassen"
|
|||
|
|
subtitle="Startbedrag voor aflossing progressie"
|
|||
|
|
subtitleColor={RED}
|
|||
|
|
onClose={() => setEditOpen(false)}
|
|||
|
|
headerBg={darkMode ? `${RED}11` : `${RED}09`}
|
|||
|
|
T={T}
|
|||
|
|
/>
|
|||
|
|
<div style={{ padding: "20px 22px" }}>
|
|||
|
|
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, display: "block", marginBottom: 6, textTransform: "uppercase" }}>Bedrag (€)</div>
|
|||
|
|
<div style={{ position: "relative" }}>
|
|||
|
|
<span style={{ position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)", fontSize: 13, color: T.muted }}>€</span>
|
|||
|
|
<input type="number" value={editVal} onChange={(e) => setEditVal(e.target.value)} autoFocus
|
|||
|
|
style={{ background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 10, color: T.text, padding: "10px 12px 10px 28px", fontSize: 14, outline: "none", width: "100%", boxSizing: "border-box" }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: "flex", gap: 10, padding: "14px 22px", borderTop: `1px solid ${T.border}` }}>
|
|||
|
|
<button onClick={() => setEditOpen(false)} style={cancelBtn(T)}>Annuleren</button>
|
|||
|
|
<button onClick={saveInitiele} style={{
|
|||
|
|
flex: 2, padding: "10px",
|
|||
|
|
background: `linear-gradient(135deg, ${RED}, #f97316)`,
|
|||
|
|
border: "none", borderRadius: 10, color: "#fff",
|
|||
|
|
fontWeight: 700, cursor: "pointer", fontSize: 13,
|
|||
|
|
}}>Opslaan</button>
|
|||
|
|
</div>
|
|||
|
|
</ModalBox>
|
|||
|
|
</Overlay>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8, alignItems: "center", flexWrap: "wrap", gap: 8 }}>
|
|||
|
|
<span style={{ fontWeight: 600 }}>Aflossing progressie</span>
|
|||
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|||
|
|
<span style={{ fontSize: 12, color: T.muted }}>Initiële schuld:</span>
|
|||
|
|
<span style={{ fontSize: 13, fontWeight: 700, color: RED }}>{fmt(schuld.initieleSchuld)}</span>
|
|||
|
|
{!locked && (
|
|||
|
|
<button onClick={() => { setEditVal(schuld.initieleSchuld || ""); setEditOpen(true); }} style={{
|
|||
|
|
padding: "4px 10px", background: `${RED}22`, border: `1px solid ${RED}44`,
|
|||
|
|
borderRadius: 7, color: RED, cursor: "pointer", fontSize: 11, fontWeight: 700,
|
|||
|
|
}}>Aanpassen</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ height: 8, background: "rgba(255,255,255,0.07)", borderRadius: 99, overflow: "hidden" }}>
|
|||
|
|
<div style={{
|
|||
|
|
height: "100%",
|
|||
|
|
width: `${Math.min(aflossingPct * 100, 100)}%`,
|
|||
|
|
background: GREEN,
|
|||
|
|
borderRadius: 99,
|
|||
|
|
transition: "width 0.5s ease",
|
|||
|
|
marginLeft: 0,
|
|||
|
|
}} />
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 6, fontSize: 11, color: T.muted }}>
|
|||
|
|
<span>€0</span>
|
|||
|
|
<span style={{ color: GREEN }}>{pct(aflossingPct)} afgelost</span>
|
|||
|
|
<span>{fmt(init)}</span>
|
|||
|
|
</div>
|
|||
|
|
</VCard>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── NieuweCategoriepopup ──────────────────────────────────────────────────────
|
|||
|
|
function NieuweCategoriepopup({ onSave, onClose, T, darkMode }) {
|
|||
|
|
const is = { background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 8, color: T.text, padding: "10px 12px", fontSize: 13, outline: "none", boxSizing: "border-box", width: "100%" };
|
|||
|
|
const COLORS = ["#ef4444","#f97316","#eab308","#a855f7","#06b6d4","#10b981","#3b82f6","#ec4899","#6b7280"];
|
|||
|
|
const [naam, setNaam] = useState("");
|
|||
|
|
const [color, setColor] = useState(COLORS[0]);
|
|||
|
|
|
|||
|
|
const save = () => {
|
|||
|
|
if (!naam.trim()) return;
|
|||
|
|
onSave({ naam: naam.trim(), color });
|
|||
|
|
onClose();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Overlay onClose={onClose}>
|
|||
|
|
<ModalBox maxWidth={400}>
|
|||
|
|
<ModalHeader icon="➕" iconBg={RED} title="Nieuwe categorie" subtitle="Voeg een schuldcategorie toe" subtitleColor={RED} onClose={onClose} headerBg={darkMode ? `${RED}11` : `${RED}09`} T={T} />
|
|||
|
|
<div style={{ padding: "20px 22px" }}>
|
|||
|
|
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", marginBottom: 6 }}>Naam *</div>
|
|||
|
|
<input autoFocus value={naam} onChange={(e) => setNaam(e.target.value)}
|
|||
|
|
onKeyDown={(e) => e.key === "Enter" && save()}
|
|||
|
|
placeholder="Bijv. Hypotheek" style={{ ...is, marginBottom: 16 }} />
|
|||
|
|
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", marginBottom: 8 }}>Kleur</div>
|
|||
|
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|||
|
|
{COLORS.map((c) => (
|
|||
|
|
<button key={c} onClick={() => setColor(c)} style={{
|
|||
|
|
width: 28, height: 28, borderRadius: "50%", background: c, border: `3px solid ${color === c ? "#fff" : "transparent"}`,
|
|||
|
|
cursor: "pointer", outline: color === c ? `2px solid ${c}` : "none", outlineOffset: 1,
|
|||
|
|
}} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: "flex", gap: 10, padding: "14px 22px", borderTop: `1px solid ${T.border}` }}>
|
|||
|
|
<button onClick={onClose} 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={save} style={{ flex: 2, padding: "10px", background: `linear-gradient(135deg, ${RED}, #f97316)`, border: "none", borderRadius: 10, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 }}>Aanmaken</button>
|
|||
|
|
</div>
|
|||
|
|
</ModalBox>
|
|||
|
|
</Overlay>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── SchuldTab (pagina) ────────────────────────────────────────────────────────
|
|||
|
|
export default function SchuldTab() {
|
|||
|
|
const { data, setData, schuld, schuldBlokken, locked, darkMode, T } = useApp();
|
|||
|
|
const [showNieuweCategorie, setShowNieuweCategorie] = useState(false);
|
|||
|
|
|
|||
|
|
const schuldCats = schuldBlokken.map((b) => ({
|
|||
|
|
...b, subtot: b.items.reduce((a, i) => a + (i.waarde || 0), 0),
|
|||
|
|
}));
|
|||
|
|
const donutTotal = schuldCats.reduce((a, b) => a + b.subtot, 0);
|
|||
|
|
|
|||
|
|
|
|||
|
|
const addSchuldBlok = ({ naam, color }) => {
|
|||
|
|
setData({
|
|||
|
|
...data,
|
|||
|
|
schuld: {
|
|||
|
|
...schuld,
|
|||
|
|
blokken: [...schuldBlokken, {
|
|||
|
|
id: `s${Date.now()}`, naam, color,
|
|||
|
|
items: [{ id: `si${Date.now()}`, naam: "Item 1", waarde: 0 }],
|
|||
|
|
}],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h1 style={{ fontSize: 20, fontWeight: 800, margin: "0 0 20px 0" }}>Schuld</h1>
|
|||
|
|
|
|||
|
|
<AflossingProgressie />
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: 20 }} />
|
|||
|
|
|
|||
|
|
{schuldCats.length > 0 && (
|
|||
|
|
<div style={{ display: "flex", gap: 16, marginBottom: 20, flexWrap: "wrap" }}>
|
|||
|
|
{/* Verdeling lijst */}
|
|||
|
|
<div style={{
|
|||
|
|
background: T.card, border: `1px solid ${T.border}`,
|
|||
|
|
borderTop: `3px solid ${RED}`, borderRadius: 12,
|
|||
|
|
padding: "16px 18px", flex: 1, minWidth: 200,
|
|||
|
|
}}>
|
|||
|
|
<div style={{ fontWeight: 700, fontSize: 12, letterSpacing: "0.1em", textTransform: "uppercase", color: RED, marginBottom: 12 }}>
|
|||
|
|
Verdeling Schuld
|
|||
|
|
</div>
|
|||
|
|
{schuldCats.map((b) => (
|
|||
|
|
<div key={b.id} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: `1px solid ${T.border}` }}>
|
|||
|
|
<span style={{ fontSize: 13, color: T.muted }}>{b.naam}</span>
|
|||
|
|
<span style={{ fontSize: 13, fontWeight: 600, color: b.color }}>{fmt(b.subtot)}</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: RED }}>{fmt(donutTotal)}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Donut */}
|
|||
|
|
<div style={{
|
|||
|
|
background: T.card, border: `1px solid ${T.border}`,
|
|||
|
|
borderTop: `3px solid ${RED}`, borderRadius: 12,
|
|||
|
|
padding: "16px 18px", flex: 1, minWidth: 200,
|
|||
|
|
display: "flex", flexDirection: "column", alignItems: "center",
|
|||
|
|
}}>
|
|||
|
|
<div style={{ fontWeight: 700, fontSize: 12, letterSpacing: "0.05em", color: T.text, marginBottom: 12, alignSelf: "flex-start" }}>
|
|||
|
|
Verdeling Schuld
|
|||
|
|
</div>
|
|||
|
|
<DonutChart
|
|||
|
|
segments={schuldCats.map((b) => ({ val: b.subtot, color: b.color }))}
|
|||
|
|
total={donutTotal} size={160}
|
|||
|
|
/>
|
|||
|
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px 14px", marginTop: 12, justifyContent: "center" }}>
|
|||
|
|
{schuldCats.map((b) => (
|
|||
|
|
<div key={b.id} style={{ display: "flex", alignItems: "center", gap: 5 }}>
|
|||
|
|
<div style={{ width: 7, height: 7, borderRadius: "50%", background: b.color }} />
|
|||
|
|
<span style={{ fontSize: 11, color: T.subtext }}>{b.naam}</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{!locked && (
|
|||
|
|
<button onClick={() => setShowNieuweCategorie(true)} style={{
|
|||
|
|
padding: "10px 20px", background: `${RED}22`,
|
|||
|
|
border: `1px solid ${RED}44`, borderRadius: 10,
|
|||
|
|
color: RED, cursor: "pointer", fontWeight: 700,
|
|||
|
|
fontSize: 13, marginBottom: 16, display: "inline-block",
|
|||
|
|
}}>+ Nieuwe categorie</button>
|
|||
|
|
)}
|
|||
|
|
{showNieuweCategorie && (
|
|||
|
|
<NieuweCategoriepopup
|
|||
|
|
onSave={addSchuldBlok}
|
|||
|
|
onClose={() => setShowNieuweCategorie(false)}
|
|||
|
|
T={T}
|
|||
|
|
darkMode={darkMode}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div style={{
|
|||
|
|
display: "grid",
|
|||
|
|
gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))",
|
|||
|
|
gap: 16, marginBottom: 20,
|
|||
|
|
}}>
|
|||
|
|
{schuldCats.map((blok) => <SchuldKaart key={blok.id} blok={blok} />)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 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: 6, padding: "6px 10px", background: "rgba(239,68,68,0.1)", borderRadius: 8, border: "1px solid rgba(239,68,68,0.25)" }}>{msg}</div>;
|
|||
|
|
}
|
|||
|
|
const deleteSmBtn = { padding: "4px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 13, flexShrink: 0 };
|
|||
|
|
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 });
|