416 lines
17 KiB
TypeScript
416 lines
17 KiB
TypeScript
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>
|
|
);
|
|
} |