ADD: added new user data model and updated the administration site

This commit is contained in:
hwinkel
2025-11-20 23:15:21 +01:00
parent 3818fbf460
commit 846a922a41
12 changed files with 766 additions and 123 deletions

View File

@@ -0,0 +1,416 @@
import React, { JSX, useEffect, useState } from "react";
import { createPlayer, deletePlayer, fetchPlayers, updatePlayer } from "../api";
import { User, UserRole } from "../../components/interfaces/users";
import { sign } from "crypto";
import { create } from "domain";
// /home/henry/code/Volleyball/frontend/src/pages/Administration/Users.tsx
type UserForm = {
name: string;
email: string;
role: string;
active: boolean;
};
const emptyForm = (): UserForm => ({
name: "",
email: "",
role: UserRole.Player,
active: true,
});
export default function UsersPage(): JSX.Element {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const pageSize = 20;
const [showForm, setShowForm] = useState(false);
const [editing, setEditing] = useState<User | null>(null);
const [form, setForm] = useState<UserForm>(emptyForm());
const [formLoading, setFormLoading] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
useEffect(() => {
const ac = new AbortController();
async function load() {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (query) params.set("q", query);
params.set("page", String(page));
params.set("limit", String(pageSize));
const token = localStorage.getItem('token');
if (!token) throw new Error("No auth token found");
const data = await fetchPlayers(token);
console.log("Loaded users:", data);
// const res = await fetch(`/api/users?${params.toString()}`, {
// signal: ac.signal,
// });
// if (!res.ok) throw new Error(`Failed to load users: ${res.status}`);
// const data = await res.json();
// Expecting { users: User[] } or User[] — handle both.
const list: User[] = Array.isArray(data) ? data : data.users ?? [];
setUsers(list);
} catch (err: any) {
if (err.name !== "AbortError") setError(err.message || String(err));
} finally {
setLoading(false);
}
}
load();
return () => ac.abort();
}, [query, page]);
function openCreate() {
setEditing(null);
setForm(emptyForm());
setShowForm(true);
}
function openEdit(u: User) {
setEditing(u);
setForm({
name: u.Username ?? "",
email: u.Email ?? "",
role: Array.isArray(u.Role) ? u.Role[0] : (u.Role ?? UserRole.Player),
active: u.IsActive ?? true,
});
setShowForm(true);
}
async function submitForm(e?: React.FormEvent) {
e?.preventDefault();
setFormLoading(true);
setError(null);
try {
const payload = { ...form };
let res: Response;
if (editing) {
res = await updatePlayer(editing.UUID, {
Username: form.name,
Email: form.email,
}, localStorage.getItem('token') || "");
// res = await createPlayer({
// Username: form.name,
// Email: form.email,
// }, localStorage.getItem('token') || "");
// res = await fetch(`/api/users/${editing.UUID}`, {
// method: "PUT",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
} else {
res = await createPlayer({
Username: form.name,
Email: form.email,
}, localStorage.getItem('token') || "");
console.log("Create response:", res);
// res = await fetch(`/api/users`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
}
// Parse response body (support both fetch Response and already-parsed results)
let saved: User;
if (res && typeof (res as any).json === "function") {
const body = await (res as Response).json();
saved = (body && (body.data ?? body)) as User;
} else {
// If `res` is already a parsed object (e.g. from axios-like helpers)
const obj = res as any;
saved = (obj && (obj.data ?? obj)) as User;
}
// Update local lists
setUsers((prev) => {
if (editing) return prev.map((p) => (p.UUID === saved.UUID ? saved : p));
return [saved, ...prev];
});
setShowForm(false);
setEditing(null);
setForm(emptyForm());
} catch (err: any) {
setError(err.message || String(err));
} finally {
setFormLoading(false);
}
}
async function removeUser(id: string) {
setDeletingId(id);
setError(null);
try {
const res = await deletePlayer(id, localStorage.getItem('token') || "");
setUsers((prev) => prev.filter((u) => u.UUID !== id));
} catch (err: any) {
setError(err.message || String(err));
console.error("Error deleting user:", err.message);
} finally {
setDeletingId(null);
setConfirmDeleteId(null);
}
}
return (
<div style={{ padding: 20 }}>
<h1 style={{ marginBottom: 8 }}>User Management</h1>
<div style={{ display: "flex", gap: 8, marginBottom: 12, alignItems: "center" }}>
<input
placeholder="Search by name or email..."
value={query}
onChange={(e) => {
setQuery(e.target.value);
setPage(1);
}}
style={{ padding: 6, flex: 1 }}
/>
<button onClick={() => { setQuery(""); setPage(1); }}>Clear</button>
<button onClick={openCreate} style={{ marginLeft: 8 }}>
New User
</button>
</div>
{error && (
<div style={{ color: "crimson", marginBottom: 12 }}>
Error: {error}
</div>
)}
<div style={{ border: "1px solid #ddd", borderRadius: 6, overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", background: "#f7f7f7" }}>
<th style={{ padding: 8 }}>Name</th>
<th style={{ padding: 8 }}>Email</th>
<th style={{ padding: 8 }}>Role</th>
<th style={{ padding: 8 }}>Active</th>
<th style={{ padding: 8 }}>Created</th>
<th style={{ padding: 8 }}>Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} style={{ padding: 12 }}>
Loading...
</td>
</tr>
) : users.length === 0 ? (
<tr>
<td colSpan={6} style={{ padding: 12 }}>
No users found.
</td>
</tr>
) : (
users.map((u) => (
<tr key={u.UUID} style={{ borderTop: "1px solid #eee" }}>
<td style={{ padding: 8 }}>{u.Username}</td>
<td style={{ padding: 8 }}>{u.Email}</td>
<td style={{ padding: 8 }}>
{u.Role &&
Array.isArray(u.Role) ? u.Role.join(", ") : (u.Role ?? "—")}
</td>
<td style={{ padding: 8 }}>{u.IsActive ? "Yes" : "No"}</td>
<td style={{ padding: 8 }}>
{u.CreatedAt ? new Date(u.CreatedAt).toLocaleString() : "—"}
</td>
<td style={{ padding: 8 }}>
<button onClick={() => openEdit(u)} style={{ marginRight: 8 }}>
Edit
</button>
<button
onClick={() => setConfirmDeleteId(u.UUID)}
disabled={deletingId === u.UUID}
style={{ color: "crimson" }}
>
{deletingId === u.UUID ? "Deleting..." : "Delete"}
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div style={{ marginTop: 12, display: "flex", gap: 8, alignItems: "center" }}>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>
Prev
</button>
<div>Page {page}</div>
<button onClick={() => setPage((p) => p + 1)} disabled={users.length < pageSize}>
Next
</button>
</div>
{showForm && (
<div
role="dialog"
aria-modal
style={{
position: "fixed",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.35)",
zIndex: 9999,
}}
onClick={() => {
// click outside to close
setShowForm(false);
setEditing(null);
}}
>
<form
onSubmit={(e) => {
e.stopPropagation();
submitForm(e);
}}
onClick={(e) => e.stopPropagation()}
style={{
width: 520,
maxWidth: "95%",
background: "white",
padding: 18,
borderRadius: 8,
boxShadow: "0 6px 20px rgba(0,0,0,0.12)",
}}
>
<h2 style={{ marginTop: 0 }}>{editing ? "Edit User" : "New User"}</h2>
<div style={{ marginBottom: 8 }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Name</label>
<input
required
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
style={{ width: "100%", padding: 8 }}
/>
</div>
<div style={{ marginBottom: 8 }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Email</label>
<input
required
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
style={{ width: "100%", padding: 8 }}
/>
</div>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<div style={{ flex: 1 }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Role</label>
<select
value={form.role}
onChange={(e) => setForm((f) => ({ ...f, role: e.target.value }))}
style={{ width: "100%", padding: 8 }}
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="coach">Coach</option>
</select>
</div>
<div style={{ minWidth: 120 }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Active</label>
<select
value={String(form.active)}
onChange={(e) =>
setForm((f) => ({ ...f, active: e.target.value === "true" }))
}
style={{ width: "100%", padding: 8 }}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
</div>
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
<button
type="button"
onClick={() => {
setShowForm(false);
setEditing(null);
}}
disabled={formLoading}
>
Cancel
</button>
<button type="submit" disabled={formLoading}>
{formLoading ? "Saving..." : editing ? "Save changes" : "Create user"}
</button>
</div>
</form>
</div>
)}
{confirmDeleteId && (
<div
role="dialog"
aria-modal
style={{
position: "fixed",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.35)",
zIndex: 10000,
}}
onClick={() => setConfirmDeleteId(null)}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: "white",
padding: 24,
borderRadius: 8,
boxShadow: "0 6px 20px rgba(0,0,0,0.12)",
minWidth: 320,
}}
>
<h3 style={{ marginTop: 0 }}>Confirm Delete</h3>
<p>Are you sure you want to delete this user? This action cannot be undone.</p>
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
<button
type="button"
onClick={() => setConfirmDeleteId(null)}
disabled={deletingId === confirmDeleteId}
>
Cancel
</button>
<button
type="button"
style={{ color: "crimson" }}
onClick={() => removeUser(confirmDeleteId)}
disabled={deletingId === confirmDeleteId}
>
{deletingId === confirmDeleteId ? "Deleting..." : "Delete"}
</button>
</div>
</div>
</div>
)}
</div>
);
}