Files
VibeFinance/frontend/src/components/GebruikersBeheer.jsx
T
2026-09-15 14:02:17 +02:00

374 lines
16 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from "react";
import { RED, GREEN } from "../constants/index.js";
import { useApp } from "../context/AppContext.jsx";
const getToken = () => localStorage.getItem("vf_token");
const authHdrs = () => ({ "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` });
async function apiFetch(path, options = {}) {
const res = await fetch(`/api${path}`, { ...options, headers: { ...authHdrs(), ...(options.headers || {}) } });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${res.status}`);
}
return res.json();
}
function fmtDatum(iso) {
if (!iso) return "—";
const d = new Date(iso);
return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`;
}
function RolSelect({ value, onChange, opties, T, darkMode }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
return (
<div ref={ref} style={{ position: "relative", width: "100%" }}>
<button type="button" onClick={() => setOpen((o) => !o)} style={{
width: "100%", background: T.inputBg, border: `1px solid ${T.inputBorder}`,
borderRadius: 8, color: T.text, padding: "8px 10px", fontSize: 13,
cursor: "pointer", display: "flex", justifyContent: "space-between", alignItems: "center",
boxSizing: "border-box",
}}>
<span>{value}</span>
<span style={{ fontSize: 10, color: T.muted }}>▼</span>
</button>
{open && (
<div style={{
position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: 999,
background: T.card, border: `1px solid ${T.inputBorder}`,
borderRadius: 8, overflow: "hidden",
boxShadow: darkMode ? "0 8px 24px rgba(0,0,0,0.5)" : "0 8px 24px rgba(0,0,0,0.15)",
}}>
{opties.map((o) => (
<div key={o} onClick={() => { onChange(o); setOpen(false); }} style={{
padding: "9px 12px", fontSize: 13, cursor: "pointer",
color: o === value ? T.accentTekst : T.text,
background: o === value ? T.inputBg : "transparent",
}}
onMouseEnter={(e) => e.currentTarget.style.background = "rgba(14,221,212,0.10)"}
onMouseLeave={(e) => e.currentTarget.style.background = o === value ? T.inputBg : "transparent"}
>
{o}
</div>
))}
</div>
)}
</div>
);
}
export default function GebruikersBeheer({ onClose }) {
const { currentUser, darkMode, T } = useApp();
const [users, setUsers] = useState([]);
const [loadErr, setLoadErr] = useState("");
const [showAdd, setShowAdd] = useState(false);
const [editUser, setEditUser] = useState(null); // user object dat bewerkt wordt
const [nieuwNaam, setNieuwNaam] = useState("");
const [nieuwEmail, setNieuwEmail] = useState("");
const [nieuwWw, setNieuwWw] = useState("");
const [nieuwRol, setNieuwRol] = useState("Viewer");
const [editNaam, setEditNaam] = useState("");
const [editEmail, setEditEmail] = useState("");
const [editWw, setEditWw] = useState("");
const [err, setErr] = useState("");
const [saving, setSaving] = useState(false);
const rollen = ["Admin", "Bewerker", "Viewer"];
useEffect(() => {
apiFetch("/users")
.then(setUsers)
.catch((e) => setLoadErr(e.message));
}, []);
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", width: "100%",
};
const addUser = async () => {
if (!nieuwNaam.trim() || !nieuwEmail.trim() || !nieuwWw.trim()) {
setErr("Naam, e-mail en wachtwoord zijn verplicht.");
return;
}
setSaving(true);
try {
const u = await apiFetch("/users", {
method: "POST",
body: JSON.stringify({ naam: nieuwNaam, email: nieuwEmail, wachtwoord: nieuwWw, rol: nieuwRol }),
});
setUsers((prev) => [...prev, u]);
setNieuwNaam(""); setNieuwEmail(""); setNieuwWw(""); setErr("");
setShowAdd(false);
} catch (e) {
setErr(e.message);
} finally {
setSaving(false);
}
};
const openEdit = (u) => {
setEditUser(u);
setEditNaam(u.naam);
setEditEmail(u.email);
setEditWw("");
setErr("");
};
const saveEdit = async () => {
if (!editNaam.trim() || !editEmail.trim()) { setErr("Naam en e-mail zijn verplicht."); return; }
setSaving(true);
try {
const patch = { naam: editNaam.trim(), email: editEmail.trim() };
if (editWw) patch.wachtwoord = editWw;
const updated = await apiFetch(`/users/${editUser.id}`, {
method: "PATCH",
body: JSON.stringify(patch),
});
setUsers((prev) => prev.map((x) => x.id === editUser.id ? { ...x, ...updated } : x));
setEditUser(null); setErr("");
} catch (e) { setErr(e.message); }
finally { setSaving(false); }
};
const toggleActief = async (u) => {
try {
const updated = await apiFetch(`/users/${u.id}`, {
method: "PATCH",
body: JSON.stringify({ actief: !u.actief }),
});
setUsers((prev) => prev.map((x) => x.id === u.id ? { ...x, ...updated } : x));
} catch (e) { setErr(e.message); }
};
const removeUser = async (id) => {
try {
await apiFetch(`/users/${id}`, { method: "DELETE" });
setUsers((prev) => prev.filter((u) => u.id !== id));
} catch (e) { setErr(e.message); }
};
const th = {
padding: "8px 12px", fontSize: 11, color: T.muted, fontWeight: 600,
textAlign: "left", borderBottom: `1px solid ${T.border}`,
background: T.card, textTransform: "uppercase", letterSpacing: "0.05em",
};
const td = { padding: "10px 12px", fontSize: 13, color: T.text, borderBottom: `1px solid ${T.border}` };
return (
<div
onClick={onClose}
style={{
position: "fixed", inset: 0, zIndex: 400,
background: "rgba(0,0,0,0.55)", backdropFilter: "blur(4px)",
display: "flex", alignItems: "center", justifyContent: "center",
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: T.popupBg, border: `1px solid ${T.border}`,
borderRadius: 16, width: "100%", maxWidth: 700,
maxHeight: "90vh", display: "flex", flexDirection: "column",
boxShadow: "0 16px 48px rgba(0,0,0,0.4)",
overflow: "hidden",
}}
>
{/* Header */}
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "18px 22px", borderBottom: `1px solid ${T.border}`, flexShrink: 0,
}}>
<div>
<span style={{ fontWeight: 700, fontSize: 15, color: T.text }}>Gebruikersbeheer</span>
<span style={{ marginLeft: 10, fontSize: 11, fontWeight: 600, color: T.accentTekst,
background: T.inputBg, borderRadius: 99, padding: "2px 8px", border: `1px solid ${T.inputBorder}`,
}}>
{users.length} gebruiker{users.length !== 1 ? "s" : ""}
</span>
</div>
<button onClick={onClose} style={{
background: "transparent", border: "none", cursor: "pointer",
color: T.muted, fontSize: 20, lineHeight: 1, padding: 4,
}}>×</button>
</div>
<div style={{ padding: "16px 22px", overflowY: "auto", flex: 1 }}>
{loadErr && (
<div style={{ color: RED, fontSize: 13, marginBottom: 12 }}>{loadErr}</div>
)}
{/* Tabel */}
<div style={{ overflowX: "auto", marginBottom: 16 }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={th}>Naam</th>
<th style={th}>E-mail</th>
<th style={th}>Aangemaakt</th>
<th style={th}>Admin</th>
<th style={th}></th>
</tr>
</thead>
<tbody>
{users.map((u) => {
const initials = u.naam.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
const isSelf = u.id === currentUser?.id || u.email === currentUser?.email;
return (
<tr key={u.id} style={{ opacity: u.actief ? 1 : 0.5 }}>
<td style={td}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: "50%",
background: T.inputBg, display: "flex",
alignItems: "center", justifyContent: "center",
fontSize: 12, fontWeight: 700, color: T.accentTekst, flexShrink: 0,
}}>
{initials}
</div>
<span style={{ fontWeight: 600 }}>{u.naam}</span>
{isSelf && (
<span style={{
fontSize: 10, fontWeight: 700, color: T.accentTekst,
background: T.inputBg, borderRadius: 99,
padding: "1px 7px", border: `1px solid ${T.inputBorder}`,
}}>jij</span>
)}
</div>
</td>
<td style={{ ...td, color: T.muted }}>{u.email}</td>
<td style={{ ...td, color: T.muted }}>{fmtDatum(u.aangemaakt)}</td>
<td style={td}>
{u.rol === "Admin"
? <span style={{ fontSize: 18 }}>⭐</span>
: <span style={{ fontSize: 13, color: T.muted }}>—</span>
}
</td>
<td style={{ ...td, whiteSpace: "nowrap" }}>
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
<button onClick={() => openEdit(u)} style={{
padding: "3px 8px", background: T.inputBg,
border: `1px solid ${T.inputBorder}`, borderRadius: 6,
color: T.accentTekst, cursor: "pointer", fontSize: 12,
}}>✏️</button>
{!isSelf && (
<>
<button onClick={() => toggleActief(u)} style={{
padding: "3px 8px", borderRadius: 6, cursor: "pointer",
fontSize: 11, fontWeight: 700,
background: u.actief ? "rgba(16,185,129,0.12)" : "rgba(239,68,68,0.1)",
border: `1px solid ${u.actief ? "rgba(16,185,129,0.4)" : "rgba(239,68,68,0.3)"}`,
color: u.actief ? GREEN : RED,
}}>
{u.actief ? "Actief" : "Inactief"}
</button>
<button onClick={() => removeUser(u.id)} style={{
padding: "3px 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,
}}>×</button>
</>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{err && (
<div style={{ color: RED, fontSize: 12, marginBottom: 12, padding: "6px 10px", background: "rgba(239,68,68,0.1)", borderRadius: 8, border: "1px solid rgba(239,68,68,0.25)" }}>
{err}
</div>
)}
{/* Bewerken modal */}
{editUser && (
<div style={{
background: darkMode ? "rgba(124,58,237,0.06)" : T.inputBg,
border: `1px solid ${T.inputBorder}`, borderRadius: 10, padding: "16px", marginBottom: 16,
}}>
<div style={{ fontSize: 11, color: T.accentTekst, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 12 }}>
{editUser.naam} bewerken
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 10 }}>
<input placeholder="Naam" value={editNaam} onChange={(e) => setEditNaam(e.target.value)} style={is} />
<input placeholder="E-mail" value={editEmail} onChange={(e) => setEditEmail(e.target.value)} style={is} />
<input placeholder="Nieuw wachtwoord (optioneel)" type="password" value={editWw} onChange={(e) => setEditWw(e.target.value)} style={{ ...is, gridColumn: "1 / -1" }} />
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => { setEditUser(null); setErr(""); }} style={{
flex: 1, padding: "8px", background: "transparent",
border: `1px solid ${T.border}`, borderRadius: 8,
color: T.muted, cursor: "pointer", fontSize: 13, fontWeight: 600,
}}>Annuleren</button>
<button onClick={saveEdit} disabled={saving} style={{
flex: 2, padding: "8px",
background: T.accentGradient,
border: "none", borderRadius: 8, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: saving ? "not-allowed" : "pointer",
}}>{saving ? "Opslaan..." : "Opslaan"}</button>
</div>
</div>
)}
{/* Nieuwe gebruiker */}
{showAdd ? (
<div style={{
background: darkMode ? "rgba(124,58,237,0.06)" : T.inputBg,
border: `1px solid ${T.border}`, borderRadius: 10, padding: "16px",
}}>
<div style={{ fontSize: 11, color: T.muted, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 12 }}>
Nieuwe gebruiker
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 10 }}>
<input placeholder="Naam" value={nieuwNaam} onChange={(e) => setNieuwNaam(e.target.value)} style={is} />
<input placeholder="E-mail" value={nieuwEmail} onChange={(e) => setNieuwEmail(e.target.value)} style={is} />
<input placeholder="Wachtwoord" type="password" value={nieuwWw} onChange={(e) => setNieuwWw(e.target.value)} style={is} />
<RolSelect value={nieuwRol} onChange={setNieuwRol} opties={rollen} T={T} darkMode={darkMode} />
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => { setShowAdd(false); setErr(""); }} style={{
flex: 1, padding: "8px", background: "transparent",
border: `1px solid ${T.border}`, borderRadius: 8,
color: T.muted, cursor: "pointer", fontSize: 13, fontWeight: 600,
}}>Annuleren</button>
<button onClick={addUser} disabled={saving} style={{
flex: 2, padding: "8px",
background: T.accentGradient,
border: "none", borderRadius: 8, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: saving ? "not-allowed" : "pointer",
}}>
{saving ? "Opslaan..." : "Toevoegen"}
</button>
</div>
</div>
) : (
<button onClick={() => setShowAdd(true)} style={{
padding: "8px 18px",
background: T.accentGradient,
border: "none", borderRadius: 8, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
}}>+ Nieuwe gebruiker</button>
)}
</div>
</div>
</div>
);
}