ADD: added new user data model and updated the administration site
This commit is contained in:
416
frontend/src/pages/Administration/Users.tsx
Normal file
416
frontend/src/pages/Administration/Users.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user