diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index fac5adb..4b543e0 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -67,6 +67,7 @@ func main() { }) api.DELETE("/players/:id", func(c *gin.Context) { player.DeletePlayer(c, db.GetDB()) + // c.JSON(http.StatusOK, gin.H{"message": "Player deleted successfully"}) }) port := os.Getenv("PORT") diff --git a/backend/internal/player/handler.go b/backend/internal/player/handler.go index c130654..df543aa 100644 --- a/backend/internal/player/handler.go +++ b/backend/internal/player/handler.go @@ -32,6 +32,7 @@ func GetPlayers(c *gin.Context, db *sql.DB) { } func CreatePlayer(c *gin.Context, db *sql.DB) { + log.Println("CreatePlayer called") var newPlayer User var err error if err := c.ShouldBindJSON(&newPlayer); err != nil { @@ -149,5 +150,6 @@ func DeletePlayer(c *gin.Context, db *sql.DB) { log.Printf("User %s (%s) deleted player with ID: %s", c.GetString("userId"), c.GetString("email"), playerID) - common.RespondMessage(c, "Player deleted successfully") + c.JSON(http.StatusOK, gin.H{"message": "Player deleted successfully"}) + // common.RespondSuccess(c, "Player deleted successfully") } diff --git a/backend/internal/player/model.go b/backend/internal/player/model.go index 964a154..05aed93 100644 --- a/backend/internal/player/model.go +++ b/backend/internal/player/model.go @@ -7,23 +7,24 @@ import ( "time" "github.com/google/uuid" + "github.com/lib/pq" ) type User struct { - UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"` - Username string `db:"username" sql:"VARCHAR(100)"` - Email string `db:"email" sql:"VARCHAR(255)" index:"true"` - lastname string `db:"lastname" sql:"VARCHAR(100)"` - fistname string `db:"fistname" sql:"VARCHAR(100)"` - password string `db:"password_hash" sql:"VARCHAR(255)"` - phone string `db:"phone" sql:"VARCHAR(20)"` - avatarURL string `db:"avatar_url" sql:"VARCHAR(255)"` - IsActive bool `db:"is_active" sql:"BOOLEAN"` - birthday time.Time `db:"birthday" sql:"DATE"` - createdAt time.Time `db:"created_at" sql:"TIMESTAMP"` - updatedAt time.Time `db:"updated_at" sql:"TIMESTAMP"` - LastLogin time.Time `db:"last_login" sql:"TIMESTAMP"` - Role []string `ignore:"true"` // wird NICHT in der DB angelegt + UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"` + Username string `db:"username" sql:"VARCHAR(100)"` + Email string `db:"email" sql:"VARCHAR(255)" index:"true"` + lastname sql.NullString `db:"lastname" sql:"VARCHAR(100)"` + firstname sql.NullString `db:"firstname" sql:"VARCHAR(100)"` + password string `db:"password_hash" sql:"VARCHAR(255)"` + phone sql.NullString `db:"phone" sql:"VARCHAR(20)"` + avatarURL sql.NullString `db:"avatar_url" sql:"VARCHAR(255)"` + IsActive sql.NullBool `db:"is_active" sql:"BOOLEAN"` + birthday *time.Time `db:"birthday" sql:"DATE"` + createdAt *time.Time `db:"created_at" sql:"TIMESTAMP"` + updatedAt *time.Time `db:"updated_at" sql:"TIMESTAMP"` + LastLogin *time.Time `db:"last_login" sql:"TIMESTAMP"` + Role []string `ignore:"true"` // wird NICHT in der DB angelegt } @@ -173,7 +174,7 @@ func savePlayer(db *sql.DB, player User) error { log.Printf("Saving player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email) - stmt := "INSERT INTO public.users (id, name, email,password_hash) VALUES ($1, $2, $3,$4)" + stmt := "INSERT INTO public.users (UUID, username, email,password_hash) VALUES ($1, $2, $3,$4)" log.Printf("Generated SQL statement: %s", stmt) _, err := db.Exec(stmt, player.UUID, player.Username, player.Email, player.password) @@ -203,7 +204,7 @@ func AddRoleToPlayer(db *sql.DB, playerID string, role []string) error { log.Printf("Adding role '%s' to player with ID: %s", role, playerID) stmt := "INSERT INTO public.roles (player_id, role) VALUES ($1, $2)" - _, err := db.Exec(stmt, playerID, role) + _, err := db.Exec(stmt, playerID, pq.Array(role)) if err != nil { log.Printf("Error adding role to player with ID %s: %v", playerID, err) return err @@ -214,7 +215,11 @@ func AddRoleToPlayer(db *sql.DB, playerID string, role []string) error { } func GetAllPlayers(db *sql.DB) ([]User, error) { - rows, err := db.Query("SELECT id, name, email FROM public.users") + // rows, err := db.Query("SELECT id, name, email FROM public.users") + // rows, err := db.Query("SELECT u.uuid, u.email, u.username, u.firstname,u.lastname,u.birthday, u.is_active, u.created_at, ur.roleFROM public.users u + // LEFT JOIN roles ur ON u.uuid::uuid = ur.player_id ORDER BY u.uuid ASC") + rows, err := db.Query(getPLayerWithRolesQuery) + if err != nil { log.Printf("Error retrieving users: %v", err) return nil, err @@ -224,7 +229,16 @@ func GetAllPlayers(db *sql.DB) ([]User, error) { var users []User for rows.Next() { var player User - if err := rows.Scan(&player.UUID, &player.Username, &player.Email); err != nil { + if err := rows.Scan(&player.UUID, + &player.Email, + &player.Username, + &player.firstname, + &player.lastname, + &player.birthday, + &player.IsActive, + &player.createdAt, + (*pq.StringArray)(&player.Role), + ); err != nil { log.Printf("Error scanning player row: %v", err) return nil, err } @@ -268,15 +282,15 @@ func GetPlayerByName(db *sql.DB, name string) (User, error) { } // DeletePlayer deletes a player from the database by ID. -func deletePlayer(db *sql.DB, id string) error { +func deletePlayer(db *sql.DB, uuid string) error { // Delete the player from the database - log.Printf("Deleting player with ID: %s", id) - _, err := db.Exec("DELETE FROM public.users WHERE id = $1", id) + log.Printf("Deleting player with ID: %s", uuid) + _, err := db.Exec("DELETE FROM public.users WHERE uuid = $1", uuid) if err != nil { - log.Printf("Error deleting player with ID %s: %v", id, err) + log.Printf("Error deleting player with ID %s: %v", uuid, err) return err } - log.Printf("User with ID %s deleted successfully", id) + log.Printf("User with ID %s deleted successfully", uuid) return nil } diff --git a/backend/internal/player/querys.go b/backend/internal/player/querys.go new file mode 100644 index 0000000..bfdcbeb --- /dev/null +++ b/backend/internal/player/querys.go @@ -0,0 +1,19 @@ +package player + +const getPLayerWithRolesQuery = ` +SELECT + u.uuid, + u.email, + u.username, + u.firstname, + u.lastname, + u.birthday, + u.is_active, + u.created_at, + ur.role +FROM public.users u +LEFT JOIN roles ur ON u.uuid = ur.player_id::uuid +` + +// GROUP BY u.uuid +// ORDER BY u.uuid ASC; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6211516..24080cf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import TeamManagement from './pages/Teams'; import ViewEditPlayer from './pages/ViewEditPlayer'; import Tournaments from './pages/Tournaments'; import NewTournament from './pages/NewTournament'; -import Administration from './pages/Administration'; +import Administration from './pages/Administration/Administration'; function App() { diff --git a/frontend/src/components/interfaces/users.tsx b/frontend/src/components/interfaces/users.tsx index e7dc9ad..879bb41 100644 --- a/frontend/src/components/interfaces/users.tsx +++ b/frontend/src/components/interfaces/users.tsx @@ -18,7 +18,7 @@ export interface User { FirstName?: string LastName?: string AvatarUrl?: string - Roles: UserRole[] + Role: UserRole[] IsActive: boolean Phone?: string TeamId?: string[] diff --git a/frontend/src/pages/Administration.tsx b/frontend/src/pages/Administration/Administration.tsx similarity index 74% rename from frontend/src/pages/Administration.tsx rename to frontend/src/pages/Administration/Administration.tsx index 1393378..f33b4f0 100644 --- a/frontend/src/pages/Administration.tsx +++ b/frontend/src/pages/Administration/Administration.tsx @@ -1,6 +1,8 @@ import React, { JSX, useEffect, useState } from "react"; -import { fetchPlayers } from "./api"; -import { User, UserRole } from "../components/interfaces/users"; +import { fetchPlayers } from "../api"; +import { User, UserRole } from "../../components/interfaces/users"; +import UsersPage from "./Users"; +import TMP from "./TMP"; // type User = { // id: string; @@ -58,10 +60,9 @@ const contentStyle: React.CSSProperties = { export default function Administration(): JSX.Element { const [activeTab, setActiveTab] = useState< - "users" | "teams" | "events" | "settings" + "users" | "teams" | "events" | "settings" | "TMP" >("users"); - const [users, setUsers] = useState([]); const [teams, setTeams] = useState([]); const [events, setEvents] = useState([]); @@ -86,27 +87,22 @@ export default function Administration(): JSX.Element { try { // Replace these endpoints with your backend API - const users = await fetchPlayers(token); - setUsers(users); - console.log("Fetched users:", users); const [uRes, tRes, eRes] = await Promise.all([ fetch("/api/admin/players"), fetch("/api/admin/teams"), fetch("/api/admin/events"), ]); - if (!uRes.ok || !tRes.ok || !eRes.ok) { + if (!tRes.ok || !eRes.ok) { throw new Error("Failed to fetch admin resources"); } // console.log(uRes); - const [uJson, tJson, eJson] = await Promise.all([ - uRes.json(), + const [tJson, eJson] = await Promise.all([ tRes.json(), eRes.json(), ]); - // setUsers(uRes as User[]); setTeams(tJson as Team[]); setEvents(eJson as EventItem[]); @@ -118,48 +114,6 @@ export default function Administration(): JSX.Element { } } - async function toggleAdmin(user: User) { - const promote = user.Roles.includes(UserRole.Admin) ? false : true; - if ( - !window.confirm( - `${promote ? "Promote" : "Demote"} ${user.Username} to ${ - promote ? "admin" : "user" - }?` - ) - ) - return; - - try { - const res = await fetch(`/api/admin/users/${user.UUID}/role`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ role: promote ? "admin" : "user" }), - }); - if (!res.ok) throw new Error("Failed to change role"); - setUsers((prev) => - prev.map((u) => (u.UUID === user.UUID ? { ...u, role: promote ? "admin" : "user" } : u)) - ); - } catch (err: any) { - alert(err?.message ?? "Failed to update role"); - } - } - - // async function toggleBan(user: User) { - // const ban = user.isActive; - // if (!window.confirm(`${ban ? "Ban" : "Unban"} ${user.username}?`)) return; - // try { - // const res = await fetch(`/api/admin/users/${user.id}/ban`, { - // method: "PATCH", - // headers: { "Content-Type": "application/json" }, - // body: JSON.stringify({ banned: ban }), - // }); - // if (!res.ok) throw new Error("Failed to update ban"); - // setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, banned: ban } : u))); - // } catch (err: any) { - // alert(err?.message ?? "Failed to update ban"); - // } - // } - async function deleteTeam(team: Team) { if (!window.confirm(`Delete team "${team.name}"? This cannot be undone.`)) return; try { @@ -240,6 +194,14 @@ export default function Administration(): JSX.Element { > Settings +
@@ -258,38 +220,8 @@ export default function Administration(): JSX.Element {

Users

Manage user accounts, roles and bans.

- {users.length === 0 ? ( -

No users found.

- ) : ( - - - - - - - - - - - - {users.map((u) => ( - - - - - {/* */} - {/* */} - - ))} - -
NameEmailRoleStatusActions
{u.Username}{u.Email}{u.Roles}{u.banned ? "Banned" : "Active"} - */} - {/* */} - {/*
- )} -
+ +
)} @@ -415,6 +347,12 @@ export default function Administration(): JSX.Element { )} + {activeTab === "TMP" && ( +
+

TMP Tab

+ +
+ )} ); diff --git a/frontend/src/pages/Administration/TMP.tsx b/frontend/src/pages/Administration/TMP.tsx new file mode 100644 index 0000000..4425dc2 --- /dev/null +++ b/frontend/src/pages/Administration/TMP.tsx @@ -0,0 +1,252 @@ +import React, { useState } from "react"; +import { createPlayer } from "../api"; +import { User } from "../../components/interfaces/users"; + +type UserRow = { [key: string]: string }; + +function parseCSV(text: string): UserRow[] { + // Basic CSV parser supporting quoted fields and commas inside quotes. + // Returns array of objects using the first row as header. + const rows: string[][] = []; + const regex = /(?:"([^"]*(?:""[^"]*)*)"|([^",\r\n]*))(,|\r?\n|$)/g; + + let row: string[] = []; + let match: RegExpExecArray | null; + let i = 0; + while ((match = regex.exec(text)) !== null) { + const quoted = match[1]; + const plain = match[2]; + const delim = match[3]; + + const value = quoted !== undefined + ? quoted.replace(/""/g, '"') + : (plain ?? ""); + row.push(value); + + if (delim === "\n" || delim === "\r\n" || delim === "") { + rows.push(row); + row = []; + } + + // safety guard to avoid infinite loops + if (++i > 10_000_000) break; + } + + if (rows.length === 0) return []; + + const header = rows[0].map(h => h.trim()); + const dataRows = rows.slice(1).filter(r => r.length > 1 || (r.length === 1 && r[0] !== "")); + + return dataRows.map(r => { + const obj: UserRow = {}; + for (let j = 0; j < header.length; j++) { + obj[header[j] ?? `col${j}`] = (r[j] ?? "").trim(); + } + return obj; + }); +} + +async function postUsersBatch(endpoint: string, users: UserRow[]) { + // const res = await fetch(endpoint, { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify({ users }), + // }); + + // if (!res.ok) { + // const text = await res.text(); + // throw new Error(`API error ${res.status}: ${text}`); + // } + // return res.json(); + const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ""); + const find = (row: UserRow, candidates: string[]) => { + const candNorm = candidates.map(c => normalize(c)); + for (const k of Object.keys(row)) { + if (candNorm.includes(normalize(k))) return (row[k] ?? "").trim(); + } + return undefined; + }; + + const mapped: User[] = users.map(r => { + const uui = find(r, ["uuid", "id", "userId"]) || ""; + const Username = find(r, ["username", "user_name", "user"]) || ""; + const Email = find(r, ["email", "e-mail"]) || ""; + const firstName = find(r, ["firstName", "first_name", "firstname", "first name"]) || ""; + const lastName = find(r, ["lastName", "last_name", "lastname", "last name"]) || ""; + const phone = find(r, ["phone", "phoneNumber", "phonenumber", "mobile"]) || undefined; + const position = find(r, ["position", "role"]) || undefined; + const jerseyNumber = find(r, ["jersey", "number", "jerseyNumber"]) || undefined; + const team = find(r, ["team", "club"]) || undefined; + const dob = find(r, ["dob", "dateOfBirth", "date_of_birth", "birthdate"]) || undefined; + + // Adjust the returned shape to match your actual User interface. + return { + uui, + Username, + Email, + firstName, + lastName, + phone, + position, + jerseyNumber, + team, + dob, + } as unknown as User; + }); + + mapped.forEach((u, idx) => { + console.log("Creating user:", idx, u); + createPlayer({ + Username: u.Username, + Email: u.Email, + }, + localStorage.getItem("token") || ""); + }); + // return await createPlayer(mapped, localStorage.getItem("token") || ""); + +} + +export default function TMP() { + const [preview, setPreview] = useState(null); + const [fileName, setFileName] = useState(null); + const [status, setStatus] = useState(null); + const [errors, setErrors] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + const onFileChange = (e: React.ChangeEvent) => { + setStatus(null); + setErrors(null); + const file = e.target.files?.[0]; + if (!file) return; + setFileName(file.name); + const reader = new FileReader(); + reader.onload = () => { + const text = String(reader.result ?? ""); + try { + const parsed = parseCSV(text); + setPreview(parsed.slice(0, 50)); // preview first 50 rows + } catch (err) { + setErrors([String(err)]); + setPreview(null); + } + }; + reader.onerror = () => { + setErrors(["Failed to read file"]); + setPreview(null); + }; + reader.readAsText(file, "utf-8"); + }; + + const validateUsers = (users: UserRow[]) => { + const problems: string[] = []; + // require "email" column (case-insensitive) + const sample = users[0] ?? {}; + const hasEmailHeader = Object.keys(sample).some(k => k.toLowerCase() === "email"); + if (!hasEmailHeader) problems.push('CSV must contain an "email" column.'); + // quick email validation for each row + const emailKey = hasEmailHeader ? Object.keys(sample).find(k => k.toLowerCase() === "email")! : null; + if (emailKey) { + users.forEach((u, idx) => { + const val = (u[emailKey] ?? "").trim(); + if (!val) problems.push(`Row ${idx + 2}: missing email`); + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) problems.push(`Row ${idx + 2}: invalid email "${val}"`); + }); + } + return { problems, emailKey }; + }; + + const onUpload = async () => { + setStatus(null); + setErrors(null); + if (!preview) { + setErrors(["No parsed data to upload. Choose a CSV file first."]); + return; + } + + // In a real flow you should re-parse the file to get all rows (we only stored preview). + // For simplicity here we assume preview contains the whole parsed dataset when small. + // If you expect large files, store entire parsed array in state instead of preview. + // For this example, we will assume preview contains all rows (adjust as needed). + const users = preview; + + const { problems, emailKey } = validateUsers(users); + if (problems.length) { + setErrors(problems); + return; + } + + setIsUploading(true); + setStatus("Uploading..."); + try { + // Adjust endpoint to match your backend API + const endpoint = "/api/users/bulk"; + // send in chunks to avoid huge payloads + const chunkSize = 100; + for (let i = 0; i < users.length; i += chunkSize) { + const chunk = users.slice(i, i + chunkSize).map(u => { + // optional: rename fields to backend expected shape + // e.g. map "firstName" -> "first_name" etc. + return u; + }); + await postUsersBatch(endpoint, chunk); + } + setStatus(`Uploaded ${users.length} users successfully.`); + } catch (err: any) { + setErrors([err.message ?? String(err)]); + setStatus("Upload failed."); + } finally { + setIsUploading(false); + } + }; + + return ( +
+

Upload users CSV

+ + + {fileName &&
Selected file: {fileName}
} + + {preview && ( + <> +

Preview (first {preview.length} rows)

+
+ + + + {Object.keys(preview[0]).map(k => ( + + ))} + + + + {preview.map((row, ri) => ( + + {Object.keys(preview[0]).map((k, ci) => ( + + ))} + + ))} + +
{k}
{row[k]}
+
+ + )} + +
+ +
+ + {status &&
{status}
} + {errors && ( +
+ Errors: +
    + {errors.map((e, i) =>
  • {e}
  • )} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Administration/Users.tsx b/frontend/src/pages/Administration/Users.tsx new file mode 100644 index 0000000..60ab496 --- /dev/null +++ b/frontend/src/pages/Administration/Users.tsx @@ -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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [query, setQuery] = useState(""); + const [page, setPage] = useState(1); + const pageSize = 20; + + const [showForm, setShowForm] = useState(false); + const [editing, setEditing] = useState(null); + const [form, setForm] = useState(emptyForm()); + const [formLoading, setFormLoading] = useState(false); + + const [deletingId, setDeletingId] = useState(null); + const [confirmDeleteId, setConfirmDeleteId] = useState(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 ( +
+

User Management

+ +
+ { + setQuery(e.target.value); + setPage(1); + }} + style={{ padding: 6, flex: 1 }} + /> + + +
+ + {error && ( +
+ Error: {error} +
+ )} + +
+ + + + + + + + + + + + + {loading ? ( + + + + ) : users.length === 0 ? ( + + + + ) : ( + users.map((u) => ( + + + + + + + + + )) + )} + +
NameEmailRoleActiveCreatedActions
+ Loading... +
+ No users found. +
{u.Username}{u.Email} + {u.Role && + Array.isArray(u.Role) ? u.Role.join(", ") : (u.Role ?? "—")} + {u.IsActive ? "Yes" : "No"} + {u.CreatedAt ? new Date(u.CreatedAt).toLocaleString() : "—"} + + + +
+
+ +
+ +
Page {page}
+ +
+ + {showForm && ( +
{ + // click outside to close + setShowForm(false); + setEditing(null); + }} + > +
{ + 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)", + }} + > +

{editing ? "Edit User" : "New User"}

+ +
+ + setForm((f) => ({ ...f, name: e.target.value }))} + style={{ width: "100%", padding: 8 }} + /> +
+ +
+ + setForm((f) => ({ ...f, email: e.target.value }))} + style={{ width: "100%", padding: 8 }} + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ )} + {confirmDeleteId && ( +
setConfirmDeleteId(null)} + > +
e.stopPropagation()} + style={{ + background: "white", + padding: 24, + borderRadius: 8, + boxShadow: "0 6px 20px rgba(0,0,0,0.12)", + minWidth: 320, + }} + > +

Confirm Delete

+

Are you sure you want to delete this user? This action cannot be undone.

+
+ + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Players.tsx b/frontend/src/pages/Players.tsx index 03e40f0..d109dca 100644 --- a/frontend/src/pages/Players.tsx +++ b/frontend/src/pages/Players.tsx @@ -11,8 +11,8 @@ import { User, UserRole } from '../components/interfaces/users'; export default function PlayerManagement() { const [players, setPlayers] = useState([]); - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); + const [Username, setName] = useState(""); + const [Email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [editingId, setEditingId] = useState(null); const token = localStorage.getItem('token'); @@ -35,14 +35,14 @@ export default function PlayerManagement() { const handleAddOrUpdate = () => { - if (!name || !email) return; + if (!Username || !Email) return; if (editingId !== null) { setPlayers(players.map(p => - p.UUID === editingId ? { ...p, name, email } : p + p.UUID === editingId ? { ...p, Username, Email } : p )); if (token) { - updatePlayer(editingId, { name, email }, token); + updatePlayer(editingId, { Username, Email }, token); } setEditingId(null); } else { @@ -50,7 +50,7 @@ export default function PlayerManagement() { UUID: "", Username:"", Email: "", - Roles: [UserRole.Player], + Role: [UserRole.Player], IsActive: true, }; @@ -89,14 +89,14 @@ export default function PlayerManagement() { setName(e.target.value)} className="border p-2 rounded" /> setEmail(e.target.value)} className="border p-2 rounded" /> diff --git a/frontend/src/pages/ViewEditPlayer.tsx b/frontend/src/pages/ViewEditPlayer.tsx index 67c0ab1..6e5619e 100644 --- a/frontend/src/pages/ViewEditPlayer.tsx +++ b/frontend/src/pages/ViewEditPlayer.tsx @@ -102,7 +102,7 @@ const ViewEditPlayer = () => { ) : ( -

{player.Roles}

+

{player.Role}

)} diff --git a/frontend/src/pages/api.tsx b/frontend/src/pages/api.tsx index 2b0ddfc..72d9351 100644 --- a/frontend/src/pages/api.tsx +++ b/frontend/src/pages/api.tsx @@ -80,7 +80,7 @@ export async function createPlayer(player: { Username: string, Email: string }, return res.json(); } -export async function updatePlayer(id: string, player: { name?: string, email?: string }, token: string) { +export async function updatePlayer(id: string, player: { Username?: string, Email?: string }, token: string) { const res = await fetch(`${API_URL}/players/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, @@ -90,11 +90,12 @@ export async function updatePlayer(id: string, player: { name?: string, email?: return res.json(); } -export async function deletePlayer(id: string, token: string) { - const res = await fetch(`${API_URL}/players/${id}`, { +export async function deletePlayer(uuid: string, token: string) { + const res = await fetch(`${API_URL}/players/${uuid}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); + console.log("Delete response:", res); if (!res.ok) throw new Error('Spieler-Löschung fehlgeschlagen'); return res.json(); }