|
|
|
@@ -1,226 +1,167 @@
|
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
|
import { createPortal } from "react-dom";
|
|
|
|
|
import { api } from "../api/client";
|
|
|
|
|
import { styles } from "../styles/styles";
|
|
|
|
|
import { stylesTokens } from "../styles/theme";
|
|
|
|
|
import { createPortal } from "react-dom";
|
|
|
|
|
|
|
|
|
|
export default function AdminPanel({ open: dashboardOpen = false, onClose }) {
|
|
|
|
|
const emptyForm = { displayName: "", email: "", password: "", role: "user", disabled: false };
|
|
|
|
|
|
|
|
|
|
export default function AdminPanel({ open: dashboardOpen = false, onClose, currentUserId }) {
|
|
|
|
|
const [users, setUsers] = useState([]);
|
|
|
|
|
|
|
|
|
|
const [userModalOpen, setUserModalOpen] = useState(false);
|
|
|
|
|
const [displayName, setDisplayName] = useState("");
|
|
|
|
|
const [email, setEmail] = useState("");
|
|
|
|
|
const [password, setPassword] = useState("");
|
|
|
|
|
const [role, setRole] = useState("user");
|
|
|
|
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
|
|
|
const [editingUser, setEditingUser] = useState(null);
|
|
|
|
|
const [form, setForm] = useState(emptyForm);
|
|
|
|
|
const [msg, setMsg] = useState("");
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!dashboardOpen && !userModalOpen) return;
|
|
|
|
|
|
|
|
|
|
const prev = document.body.style.overflow;
|
|
|
|
|
if (!dashboardOpen && !editorOpen) return;
|
|
|
|
|
const previous = document.body.style.overflow;
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.body.style.overflow = prev;
|
|
|
|
|
};
|
|
|
|
|
}, [dashboardOpen, userModalOpen]);
|
|
|
|
|
return () => { document.body.style.overflow = previous; };
|
|
|
|
|
}, [dashboardOpen, editorOpen]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!dashboardOpen) {
|
|
|
|
|
setUserModalOpen(false);
|
|
|
|
|
setEditorOpen(false);
|
|
|
|
|
setMsg("");
|
|
|
|
|
}
|
|
|
|
|
}, [dashboardOpen]);
|
|
|
|
|
|
|
|
|
|
const loadUsers = async () => {
|
|
|
|
|
const u = await api("/admin/users");
|
|
|
|
|
setUsers(u);
|
|
|
|
|
};
|
|
|
|
|
const loadUsers = async () => setUsers(await api("/admin/users"));
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadUsers().catch(() => {});
|
|
|
|
|
}, []);
|
|
|
|
|
useEffect(() => { loadUsers().catch(() => {}); }, []);
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
setDisplayName("");
|
|
|
|
|
setEmail("");
|
|
|
|
|
setPassword("");
|
|
|
|
|
setRole("user");
|
|
|
|
|
};
|
|
|
|
|
const setField = (key, value) => setForm((current) => ({ ...current, [key]: value }));
|
|
|
|
|
|
|
|
|
|
const createUser = async () => {
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
setEditingUser(null);
|
|
|
|
|
setForm({ ...emptyForm });
|
|
|
|
|
setMsg("");
|
|
|
|
|
setEditorOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEdit = (user) => {
|
|
|
|
|
setEditingUser(user);
|
|
|
|
|
setForm({
|
|
|
|
|
displayName: user.display_name || "",
|
|
|
|
|
email: user.email || "",
|
|
|
|
|
password: "",
|
|
|
|
|
role: user.role || "user",
|
|
|
|
|
disabled: !!user.disabled,
|
|
|
|
|
});
|
|
|
|
|
setMsg("");
|
|
|
|
|
setEditorOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeEditor = () => {
|
|
|
|
|
setEditorOpen(false);
|
|
|
|
|
setEditingUser(null);
|
|
|
|
|
setMsg("");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveUser = async () => {
|
|
|
|
|
setMsg("");
|
|
|
|
|
if (!form.email.trim()) return setMsg("❌ E-Mail ist erforderlich.");
|
|
|
|
|
if (!editingUser && form.password.length < 8) return setMsg("❌ Passwort muss mindestens 8 Zeichen haben.");
|
|
|
|
|
if (editingUser && form.password && form.password.length < 8) return setMsg("❌ Neues Passwort muss mindestens 8 Zeichen haben.");
|
|
|
|
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
await api("/admin/users", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ display_name: displayName, email, password, role }),
|
|
|
|
|
const payload = {
|
|
|
|
|
display_name: form.displayName,
|
|
|
|
|
email: form.email,
|
|
|
|
|
role: form.role,
|
|
|
|
|
disabled: form.disabled,
|
|
|
|
|
};
|
|
|
|
|
if (form.password) payload.password = form.password;
|
|
|
|
|
|
|
|
|
|
await api(editingUser ? `/admin/users/${editingUser.id}` : "/admin/users", {
|
|
|
|
|
method: editingUser ? "PATCH" : "POST",
|
|
|
|
|
body: JSON.stringify(payload),
|
|
|
|
|
});
|
|
|
|
|
setMsg("✅ User erstellt.");
|
|
|
|
|
await loadUsers();
|
|
|
|
|
resetForm();
|
|
|
|
|
setUserModalOpen(false);
|
|
|
|
|
closeEditor();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setMsg("❌ Fehler: " + (e?.message || "unknown"));
|
|
|
|
|
setMsg("❌ " + (e?.message || "Speichern fehlgeschlagen."));
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteUser = async (u) => {
|
|
|
|
|
if (!window.confirm(`User wirklich löschen (deaktivieren)?\n\n${u.display_name || u.email}`)) return;
|
|
|
|
|
const disableUser = async (user) => {
|
|
|
|
|
if (user.id === currentUserId) return;
|
|
|
|
|
if (!window.confirm(`User wirklich deaktivieren?\n\n${user.display_name || user.email}`)) return;
|
|
|
|
|
try {
|
|
|
|
|
await api(`/admin/users/${u.id}`, { method: "DELETE" });
|
|
|
|
|
await api(`/admin/users/${user.id}`, { method: "DELETE" });
|
|
|
|
|
await loadUsers();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert("Fehler: " + (e?.message || "unknown"));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
setUserModalOpen(false);
|
|
|
|
|
setMsg("");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!dashboardOpen) return null;
|
|
|
|
|
|
|
|
|
|
return createPortal(
|
|
|
|
|
<div style={styles.modalOverlay} onMouseDown={onClose}>
|
|
|
|
|
<div
|
|
|
|
|
style={{ ...styles.modalCard, width: "min(760px, 100%)", padding: 0 }}
|
|
|
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ ...styles.adminWrap, marginTop: 0, border: "none", boxShadow: "none", backdropFilter: "none", WebkitBackdropFilter: "none" }}>
|
|
|
|
|
<div style={styles.adminTop}>
|
|
|
|
|
<div style={styles.adminTitle}>Admin Dashboard</div>
|
|
|
|
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
|
|
|
<button onClick={() => setUserModalOpen(true)} style={styles.primaryBtn}>
|
|
|
|
|
+ User anlegen
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Dashboard schließen">
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ marginTop: 12, fontWeight: 900, color: stylesTokens.textGold }}>
|
|
|
|
|
Vorhandene User
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ marginTop: 8, display: "grid", gap: 8 }}>
|
|
|
|
|
{users.map((u) => (
|
|
|
|
|
<div
|
|
|
|
|
key={u.id}
|
|
|
|
|
className="hp-admin-user-row"
|
|
|
|
|
style={{
|
|
|
|
|
...styles.userRow,
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="hp-admin-name" style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
|
|
|
|
|
{u.display_name || "—"}
|
|
|
|
|
<div style={{ ...styles.modalCard, width: "min(780px, 100%)", padding: 0 }} onMouseDown={(e) => e.stopPropagation()}>
|
|
|
|
|
<div style={{ padding: "18px 18px 16px" }}>
|
|
|
|
|
<div style={styles.modalHeader}>
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold, fontSize: 19 }}>Admin Dashboard</div>
|
|
|
|
|
<div style={{ marginTop: 3, color: stylesTokens.textDim, fontSize: 12 }}>Benutzer, Rollen und Zugangsdaten verwalten</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="hp-admin-email" style={{ color: stylesTokens.textDim, fontSize: 13 }}>{u.email}</div>
|
|
|
|
|
<div className="hp-admin-role" style={{ textAlign: "center", fontWeight: 900, color: stylesTokens.textGold }}>
|
|
|
|
|
{u.role}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`hp-admin-status ${u.disabled ? "hp-admin-status--disabled" : ""}`} style={{ textAlign: "center", opacity: 0.85, color: stylesTokens.textMain }}>
|
|
|
|
|
{u.disabled ? "disabled" : "active"}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
className="hp-admin-action"
|
|
|
|
|
onClick={() => deleteUser(u)}
|
|
|
|
|
style={{
|
|
|
|
|
...styles.secondaryBtn,
|
|
|
|
|
padding: "8px 10px",
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
color: "#ffb3b3",
|
|
|
|
|
opacity: u.role === "admin" ? 0.4 : 1,
|
|
|
|
|
pointerEvents: u.role === "admin" ? "none" : "auto",
|
|
|
|
|
}}
|
|
|
|
|
title={u.role === "admin" ? "Admin kann nicht gelöscht werden" : "User löschen (deaktivieren)"}
|
|
|
|
|
>
|
|
|
|
|
Löschen
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Dashboard schließen">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{userModalOpen &&
|
|
|
|
|
createPortal(
|
|
|
|
|
<div style={styles.modalOverlay} onMouseDown={closeModal}>
|
|
|
|
|
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
|
|
|
|
<div style={styles.modalHeader}>
|
|
|
|
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>
|
|
|
|
|
Neuen User anlegen
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={closeModal} style={styles.modalCloseBtn} aria-label="Schließen">
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginTop: 18, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
|
|
|
|
|
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>Vorhandene User <span style={{ color: stylesTokens.textDim, fontWeight: 700 }}>({users.length})</span></div>
|
|
|
|
|
<button onClick={openCreate} style={styles.primaryBtn}>+ User anlegen</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
marginTop: 12,
|
|
|
|
|
display: "grid",
|
|
|
|
|
gap: 8,
|
|
|
|
|
justifyItems: "center", // <<< zentriert alles
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
value={displayName}
|
|
|
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
|
|
|
|
placeholder="Name (z.B. Sascha)"
|
|
|
|
|
style={styles.input}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
value={email}
|
|
|
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
|
|
|
placeholder="Email"
|
|
|
|
|
style={styles.input}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
value={password}
|
|
|
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
|
|
|
placeholder="Initial Passwort"
|
|
|
|
|
type="password"
|
|
|
|
|
style={styles.input}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<select value={role} onChange={(e) => setRole(e.target.value)} style={styles.input}>
|
|
|
|
|
<option value="user">user</option>
|
|
|
|
|
<option value="admin">admin</option>
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
{msg && <div style={{ opacity: 0.9, color: stylesTokens.textMain }}>{msg}</div>}
|
|
|
|
|
|
|
|
|
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
resetForm();
|
|
|
|
|
setMsg("");
|
|
|
|
|
}}
|
|
|
|
|
style={styles.secondaryBtn}
|
|
|
|
|
>
|
|
|
|
|
Leeren
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={createUser} style={styles.primaryBtn}>
|
|
|
|
|
User erstellen
|
|
|
|
|
<div style={{ marginTop: 10, display: "grid", gap: 9 }}>
|
|
|
|
|
{users.map((user) => (
|
|
|
|
|
<div key={user.id} className="hp-admin-user-row" style={{ ...styles.userRow, alignItems: "center" }}>
|
|
|
|
|
<div className="hp-admin-name" style={{ color: stylesTokens.textMain, fontWeight: 900 }}>{user.display_name || "—"}</div>
|
|
|
|
|
<div className="hp-admin-email" style={{ color: stylesTokens.textDim, fontSize: 13 }}>{user.email}</div>
|
|
|
|
|
<div className="hp-admin-role" style={{ textAlign: "center", fontWeight: 900, color: stylesTokens.textGold }}>{user.role}</div>
|
|
|
|
|
<div className={`hp-admin-status ${user.disabled ? "hp-admin-status--disabled" : ""}`} style={{ textAlign: "center", opacity: 0.85 }}>{user.disabled ? "disabled" : "active"}</div>
|
|
|
|
|
<div className="hp-admin-actions">
|
|
|
|
|
<button className="hp-admin-action" onClick={() => openEdit(user)} style={{ ...styles.secondaryBtn, padding: "8px 11px" }}>Bearbeiten</button>
|
|
|
|
|
<button className="hp-admin-action" onClick={() => disableUser(user)} disabled={user.id === currentUserId || user.disabled} style={{ ...styles.secondaryBtn, padding: "8px 11px", color: "#ffb3b3" }}>
|
|
|
|
|
{user.disabled ? "Deaktiviert" : "Deaktivieren"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
|
|
|
|
Tipp: Name wird in TopBar & Siegeranzeige genutzt.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>,
|
|
|
|
|
document.body
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{editorOpen && createPortal(
|
|
|
|
|
<div style={styles.modalOverlay} onMouseDown={closeEditor}>
|
|
|
|
|
<div style={{ ...styles.modalCard, width: "min(470px, 100%)" }} onMouseDown={(e) => e.stopPropagation()}>
|
|
|
|
|
<div style={styles.modalHeader}>
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold, fontSize: 18 }}>{editingUser ? "User bearbeiten" : "Neuen User anlegen"}</div>
|
|
|
|
|
<div style={{ marginTop: 3, color: stylesTokens.textDim, fontSize: 12 }}>{editingUser ? "Profil und Zugangsdaten aktualisieren" : "Ein neues Benutzerkonto erstellen"}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={closeEditor} style={styles.modalCloseBtn} aria-label="Schließen">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ marginTop: 18, display: "grid", gap: 10 }}>
|
|
|
|
|
<label style={styles.adminFieldLabel}>Anzeigename<input value={form.displayName} onChange={(e) => setField("displayName", e.target.value)} placeholder="z. B. Sascha Nesterovic" style={styles.input} autoFocus /></label>
|
|
|
|
|
<label style={styles.adminFieldLabel}>E-Mail<input value={form.email} onChange={(e) => setField("email", e.target.value)} placeholder="name@example.com" style={styles.input} inputMode="email" /></label>
|
|
|
|
|
<label style={styles.adminFieldLabel}>{editingUser ? "Neues Passwort (optional)" : "Passwort"}<input value={form.password} onChange={(e) => setField("password", e.target.value)} placeholder={editingUser ? "Leer lassen = unverändert" : "Mindestens 8 Zeichen"} type="password" style={styles.input} /></label>
|
|
|
|
|
<label style={styles.adminFieldLabel}>Rolle<select value={form.role} onChange={(e) => setField("role", e.target.value)} disabled={editingUser?.id === currentUserId} style={styles.input}><option value="user">User</option><option value="admin">Admin</option></select></label>
|
|
|
|
|
{editingUser && <label className="hp-admin-check"><input type="checkbox" checked={!form.disabled} onChange={(e) => setField("disabled", !e.target.checked)} /> Konto ist aktiv</label>}
|
|
|
|
|
{msg && <div style={{ color: "#ffb3b3", fontSize: 13 }}>{msg}</div>}
|
|
|
|
|
<button onClick={saveUser} style={{ ...styles.primaryBtn, width: "100%", marginTop: 4 }} disabled={saving}>{saving ? "Speichern …" : editingUser ? "Änderungen speichern" : "User erstellen"}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>,
|
|
|
|
|
document.body
|
|
|
|
|
)}
|
|
|
|
|
</div>,
|
|
|
|
|
document.body
|
|
|
|
|
);
|
|
|
|
|