ADD: added function to update db scheme automaticly

This commit is contained in:
hwinkel
2025-11-18 22:23:08 +01:00
parent ef396af480
commit 3818fbf460
15 changed files with 876 additions and 186 deletions

View File

@@ -10,6 +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';
function App() {
@@ -26,6 +27,7 @@ function App() {
<Route path="/players/:id" element={<ViewEditPlayer />} />
<Route path="/teams" element={<ProtectedRoute><TeamManagement /></ProtectedRoute>} />
<Route path="/Administration" element={<ProtectedRoute><Administration /></ProtectedRoute>} />
<Route path="/tournament/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
<Route path="/tournament/new" element={<ProtectedRoute><NewTournament /></ProtectedRoute>} />

View File

@@ -0,0 +1,60 @@
/**
* Interfaces and enums for user-related types
* File: /home/henry/code/Volleyball/frontend/src/components/interfaces/users.tsx
*/
export enum UserRole {
Admin = 'admin',
Coach = 'coach',
Player = 'player',
}
/** Primary user model returned from the API */
export interface User {
UUID: string
Username: string
Email: string
FirstName?: string
LastName?: string
AvatarUrl?: string
Roles: UserRole[]
IsActive: boolean
Phone?: string
TeamId?: string[]
CreatedAt?: string // ISO date
UpdatedAt?: string // ISO date
LastLogin?: string // ISO date
}
/** Data required to create a new user */
export interface CreateUserDTO {
username: string
email: string
password: string
firstName?: string
lastName?: string
roles?: UserRole[]
}
/** Data for partial updates to a user */
export interface UpdateUserDTO {
username?: string
email?: string
password?: string
firstName?: string | null
lastName?: string | null
avatarUrl?: string | null
roles?: UserRole[]
isActive?: boolean
teamId?: string | null
metadata?: Record<string, unknown> | null
}
/** Simple auth state slice for frontend state management */
export interface AuthState {
currentUser?: User | null
token?: string | null
isLoading: boolean
error?: string | null
}

View File

@@ -0,0 +1,421 @@
import React, { JSX, useEffect, useState } from "react";
import { fetchPlayers } from "./api";
import { User, UserRole } from "../components/interfaces/users";
// type User = {
// id: string;
// name: string;
// email: string;
// role: "user" | "admin";
// banned?: boolean;
// };
type Team = {
id: string;
name: string;
members: number;
};
type EventItem = {
id: string;
title: string;
date: string; // ISO
location?: string;
};
const containerStyle: React.CSSProperties = {
display: "flex",
height: "100%",
gap: 20,
padding: 20,
boxSizing: "border-box",
fontFamily: "Inter, Roboto, Arial, sans-serif",
};
const sidebarStyle: React.CSSProperties = {
width: 220,
borderRight: "1px solid #e6e6e6",
paddingRight: 12,
};
const tabButtonStyle = (active: boolean): React.CSSProperties => ({
display: "block",
width: "100%",
textAlign: "left",
padding: "8px 10px",
marginBottom: 6,
cursor: "pointer",
borderRadius: 6,
background: active ? "#0b5fff1a" : "transparent",
border: active ? "1px solid #0b5fff33" : "1px solid transparent",
});
const contentStyle: React.CSSProperties = {
flex: 1,
minWidth: 0,
overflowY: "auto",
};
export default function Administration(): JSX.Element {
const [activeTab, setActiveTab] = useState<
"users" | "teams" | "events" | "settings"
>("users");
const [users, setUsers] = useState<User[]>([]);
const [teams, setTeams] = useState<Team[]>([]);
const [events, setEvents] = useState<EventItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Form state for creating event
const [newEventTitle, setNewEventTitle] = useState("");
const [newEventDate, setNewEventDate] = useState("");
const [newEventLocation, setNewEventLocation] = useState("");
const token = localStorage.getItem('token');
useEffect(() => {
// fetch all admin resources when component mounts
if (token) refreshAll();
}, []);
async function refreshAll() {
if (!token) return;
setLoading(true);
setError(null);
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) {
throw new Error("Failed to fetch admin resources");
}
// console.log(uRes);
const [uJson, tJson, eJson] = await Promise.all([
uRes.json(),
tRes.json(),
eRes.json(),
]);
// setUsers(uRes as User[]);
setTeams(tJson as Team[]);
setEvents(eJson as EventItem[]);
} catch (err: any) {
setError(err?.message ?? "Unknown error");
} finally {
setLoading(false);
}
}
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 {
const res = await fetch(`/api/admin/teams/${team.id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Failed to delete team");
setTeams((prev) => prev.filter((t) => t.id !== team.id));
} catch (err: any) {
alert(err?.message ?? "Failed to delete team");
}
}
async function createEvent(e: React.FormEvent) {
e.preventDefault();
if (!newEventTitle || !newEventDate) {
alert("Title and date required");
return;
}
try {
const payload = {
title: newEventTitle,
date: new Date(newEventDate).toISOString(),
location: newEventLocation || undefined,
};
const res = await fetch("/api/admin/events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error("Failed to create event");
const created: EventItem = await res.json();
setEvents((prev) => [created, ...prev]);
setNewEventTitle("");
setNewEventDate("");
setNewEventLocation("");
setActiveTab("events");
} catch (err: any) {
alert(err?.message ?? "Failed to create event");
}
}
async function deleteEvent(ev: EventItem) {
if (!window.confirm(`Delete event "${ev.title}"?`)) return;
try {
const res = await fetch(`/api/admin/events/${ev.id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Failed to delete event");
setEvents((prev) => prev.filter((x) => x.id !== ev.id));
} catch (err: any) {
alert(err?.message ?? "Failed to delete event");
}
}
return (
<div style={containerStyle}>
<aside style={sidebarStyle}>
<h3>Administration</h3>
<div>
<button
style={tabButtonStyle(activeTab === "users")}
onClick={() => setActiveTab("users")}
>
Users
</button>
<button
style={tabButtonStyle(activeTab === "teams")}
onClick={() => setActiveTab("teams")}
>
Teams
</button>
<button
style={tabButtonStyle(activeTab === "events")}
onClick={() => setActiveTab("events")}
>
Events
</button>
<button
style={tabButtonStyle(activeTab === "settings")}
onClick={() => setActiveTab("settings")}
>
Settings
</button>
</div>
<div style={{ marginTop: 20 }}>
<button onClick={refreshAll} style={{ padding: "6px 10px", cursor: "pointer" }}>
Refresh
</button>
</div>
{loading && <p style={{ marginTop: 12 }}>Loading...</p>}
{error && <p style={{ marginTop: 12, color: "red" }}>{error}</p>}
</aside>
<main style={contentStyle}>
{activeTab === "users" && (
<section>
<h2>Users</h2>
<p>Manage user accounts, roles and bans.</p>
<div style={{ marginTop: 12 }}>
{users.length === 0 ? (
<p>No users found.</p>
) : (
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", borderBottom: "1px solid #eee" }}>
<th style={{ padding: "8px 6px" }}>Name</th>
<th style={{ padding: "8px 6px" }}>Email</th>
<th style={{ padding: "8px 6px" }}>Role</th>
<th style={{ padding: "8px 6px" }}>Status</th>
<th style={{ padding: "8px 6px" }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.UUID} style={{ borderBottom: "1px solid #fafafa" }}>
<td style={{ padding: "8px 6px" }}>{u.Username}</td>
<td style={{ padding: "8px 6px" }}>{u.Email}</td>
<td style={{ padding: "8px 6px" }}>{u.Roles}</td>
{/* <td style={{ padding: "8px 6px" }}>{u.banned ? "Banned" : "Active"}</td> */}
{/* <td style={{ padding: "8px 6px" }}>
<button onClick={() => toggleAdmin(u)} style={{ marginRight: 8 }}>
{u.roles.includes(UserRole.Admin) ? "Demote" : "Promote"}
</button> */}
{/* <button onClick={() => toggleBan(u)}>{u.banned ? "Unban" : "Ban"}</button> */}
{/* </td> */}
</tr>
))}
</tbody>
</table>
)}
</div>
</section>
)}
{activeTab === "teams" && (
<section>
<h2>Teams</h2>
<p>Manage teams and membership.</p>
<div style={{ marginTop: 12 }}>
{teams.length === 0 ? (
<p>No teams found.</p>
) : (
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
{teams.map((t) => (
<li
key={t.id}
style={{
display: "flex",
justifyContent: "space-between",
padding: "8px 0",
borderBottom: "1px solid #f2f2f2",
}}
>
<div>
<strong>{t.name}</strong>
<div style={{ fontSize: 13, color: "#666" }}>{t.members} members</div>
</div>
<div>
<button style={{ marginRight: 8 }}>Edit</button>
<button onClick={() => deleteTeam(t)}>Delete</button>
</div>
</li>
))}
</ul>
)}
</div>
</section>
)}
{activeTab === "events" && (
<section>
<h2>Events</h2>
<p>Create and manage events</p>
<form onSubmit={createEvent} style={{ marginTop: 12, marginBottom: 24 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
placeholder="Event title"
value={newEventTitle}
onChange={(e) => setNewEventTitle(e.target.value)}
style={{ padding: 8, flex: 1 }}
/>
<input
type="date"
value={newEventDate}
onChange={(e) => setNewEventDate(e.target.value)}
style={{ padding: 8 }}
/>
<input
placeholder="Location (optional)"
value={newEventLocation}
onChange={(e) => setNewEventLocation(e.target.value)}
style={{ padding: 8 }}
/>
<button type="submit" style={{ padding: "8px 12px" }}>
Create
</button>
</div>
</form>
<div>
{events.length === 0 ? (
<p>No events.</p>
) : (
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
{events.map((ev) => (
<li
key={ev.id}
style={{
display: "flex",
justifyContent: "space-between",
padding: "8px 0",
borderBottom: "1px solid #f2f2f2",
}}
>
<div>
<strong>{ev.title}</strong>
<div style={{ fontSize: 13, color: "#666" }}>
{new Date(ev.date).toLocaleString()} {ev.location ? `${ev.location}` : ""}
</div>
</div>
<div>
<button style={{ marginRight: 8 }}>Edit</button>
<button onClick={() => deleteEvent(ev)}>Delete</button>
</div>
</li>
))}
</ul>
)}
</div>
</section>
)}
{activeTab === "settings" && (
<section>
<h2>Settings</h2>
<p>Application-wide administrative settings.</p>
<div style={{ marginTop: 12 }}>
<p style={{ fontSize: 14, color: "#444" }}>
Use these settings to control global features. Implement actual controls as needed.
</p>
<div style={{ marginTop: 12 }}>
<label style={{ display: "block", marginBottom: 8 }}>
<input type="checkbox" /> Allow public registration
</label>
<label style={{ display: "block", marginBottom: 8 }}>
<input type="checkbox" /> Require email verification
</label>
<div style={{ marginTop: 12 }}>
<button onClick={() => alert("Settings saved (stub)")}>Save settings</button>
</div>
</div>
</div>
</section>
)}
</main>
</div>
);
}

View File

@@ -15,9 +15,10 @@ export default function Navigation() {
</div>
<div>
{token ? (
<><Link to="/Administration" className="px-3 py-1 border rounded mr-2">Admin</Link>
<button onClick={logout} className="bg-red-500 px-3 py-1 rounded">
Logout
</button>
</button></>
) : (
<>
<Link to="/register" className="px-3 py-1 border rounded">Register</Link>

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useState } from 'react';
import { fetchPlayers,createPlayer,deletePlayer,updatePlayer } from './api';
import { Navigate, useNavigate } from 'react-router-dom';
import { User, UserRole } from '../components/interfaces/users';
interface Player {
id: string;
name: string;
email: string;
}
// interface Player {
// id: string;
// name: string;
// email: string;
// }
export default function PlayerManagement() {
const [players, setPlayers] = useState<Player[]>([]);
const [players, setPlayers] = useState<User[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -39,17 +39,19 @@ export default function PlayerManagement() {
if (editingId !== null) {
setPlayers(players.map(p =>
p.id === editingId ? { ...p, name, email } : p
p.UUID === editingId ? { ...p, name, email } : p
));
if (token) {
updatePlayer(editingId, { name, email }, token);
}
setEditingId(null);
} else {
const newPlayer: Player = {
id: "",
name,
email,
const newPlayer: User = {
UUID: "",
Username:"",
Email: "",
Roles: [UserRole.Player],
IsActive: true,
};
if (token) {
@@ -68,7 +70,7 @@ export default function PlayerManagement() {
};
const handleDelete = (id: string) => {
setPlayers(players.filter(p => p.id !== id));
setPlayers(players.filter(p => p.UUID !== id));
if (token) {
deletePlayer(id, token);
}
@@ -124,19 +126,19 @@ export default function PlayerManagement() {
</thead>
<tbody>
{players.map(player => (
<tr key={player.id}>
<td className="border px-4 py-2">{player.name}</td>
<td className="border px-4 py-2">{player.email}</td>
<tr key={player.UUID}>
<td className="border px-4 py-2">{player.Username}</td>
<td className="border px-4 py-2">{player.Email}</td>
<td className="border px-4 py-2 space-x-2">
<button
key={player.id}
onClick={handleViewEdit(player.id)}
key={player.UUID}
onClick={handleViewEdit(player.UUID)}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(player.id)}
onClick={() => handleDelete(player.UUID)}
className="bg-red-500 text-white px-2 py-1 rounded"
>
Löschen

View File

@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { fetchTournament, updateTournament, registerTeam } from './api';
import { useAuth } from './AuthContext';
import { Tournament } from './types';
import { Tournament } from '../components/interfaces/types';
export default function TournamentDetails() {
const { id } = useParams<{ id: string }>();

View File

@@ -1,7 +1,7 @@
import { Link, useLocation } from 'react-router-dom';
import { use, useEffect, useState } from 'react';
import { fetchTournaments, fetchTournament } from './api'; // Importiere die API-Funktion
import type { Tournament } from './types';
import type { Tournament } from '../components/interfaces/types';
function useQuery() {
return new URLSearchParams(useLocation().search);

View File

@@ -2,13 +2,8 @@ import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getUserFromToken } from '../components/utils/jwt'; // Importiere die Funktion zum Decodieren des Tokens
import {fetchPlayer} from './api'; // Importiere die Funktion zum Abrufen des Spielers
import { User } from '../components/interfaces/users';
interface Player {
id: string;
name: string;
email: string;
role: string;
}
const ViewEditPlayer = () => {
const { id } = useParams<{ id: string }>();
@@ -16,7 +11,7 @@ const ViewEditPlayer = () => {
const currentUser = token ? getUserFromToken(token) : null;
const isAdmin = currentUser?.role === 'admin';
const [player, setPlayer] = useState<Player | null>(null);
const [player, setPlayer] = useState<User | null>(null);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('');
@@ -32,9 +27,9 @@ const ViewEditPlayer = () => {
const data = await fetchPlayer(token, id);
console.log("Geladener Spieler:", data);
setPlayer(data);
setName(data.name);
setEmail(data.email);
setRole(data.role);
setName(data.Username);
setEmail(data.Email);
setRole(data.Roles);
} catch (error) {
setMessage('Spieler konnte nicht geladen werden.');
}
@@ -77,7 +72,7 @@ const ViewEditPlayer = () => {
onChange={(e) => setName(e.target.value)}
/>
) : (
<p>{player.name}</p>
<p>{player.Username}</p>
)}
</div>
@@ -91,7 +86,7 @@ const ViewEditPlayer = () => {
onChange={(e) => setEmail(e.target.value)}
/>
) : (
<p>{player.email}</p>
<p>{player.Email}</p>
)}
</div>
@@ -107,7 +102,7 @@ const ViewEditPlayer = () => {
<option value="admin">Admin</option>
</select>
) : (
<p>{player.role}</p>
<p>{player.Roles}</p>
)}
</div>

View File

@@ -70,7 +70,7 @@ export async function fetchPlayers(token: string) {
return res.json();
}
export async function createPlayer(player: { name: string, email: string }, token: string) {
export async function createPlayer(player: { Username: string, Email: string }, token: string) {
const res = await fetch(`${API_URL}/players`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },