Files
VibeFinance/frontend/src/components/GebruikersBeheer.jsx
T

374 lines
16 KiB
React
Raw Normal View History

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",
2026-09-15 14:02:17 +02:00
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)"}
2026-09-15 14:02:17 +02:00
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>
2026-09-15 14:02:17 +02:00
<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%",
2026-09-15 14:02:17 +02:00
background: T.inputBg, display: "flex",
alignItems: "center", justifyContent: "center",
2026-09-15 14:02:17 +02:00
fontSize: 12, fontWeight: 700, color: T.accentTekst, flexShrink: 0,
}}>
{initials}
</div>
<span style={{ fontWeight: 600 }}>{u.naam}</span>
{isSelf && (
<span style={{
2026-09-15 14:02:17 +02:00
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={{
2026-09-15 14:02:17 +02:00
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={{
2026-09-15 14:02:17 +02:00
background: darkMode ? "rgba(124,58,237,0.06)" : T.inputBg,
border: `1px solid ${T.inputBorder}`, borderRadius: 10, padding: "16px", marginBottom: 16,
}}>
2026-09-15 14:02:17 +02:00
<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",
2026-09-15 14:02:17 +02:00
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={{
2026-09-15 14:02:17 +02:00
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",
2026-09-15 14:02:17 +02:00
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",
2026-09-15 14:02:17 +02:00
background: T.accentGradient,
border: "none", borderRadius: 8, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
}}>+ Nieuwe gebruiker</button>
)}
</div>
</div>
</div>
);
}