ADD: team site and player/usermanagement is working
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"volleyball/internal/auth"
|
||||
"volleyball/internal/database"
|
||||
@@ -63,12 +64,16 @@ func main() {
|
||||
player.CreatePlayer(c, db.GetDB())
|
||||
})
|
||||
api.PUT("/players/:id", func(c *gin.Context) {
|
||||
log.Println("PUT /players/:id called", c.Params)
|
||||
player.UpdatePlayer(c, db.GetDB())
|
||||
})
|
||||
api.DELETE("/players/:id", func(c *gin.Context) {
|
||||
player.DeletePlayer(c, db.GetDB())
|
||||
// c.JSON(http.StatusOK, gin.H{"message": "Player deleted successfully"})
|
||||
})
|
||||
api.GET("/teams", func(c *gin.Context) {
|
||||
|
||||
})
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
|
||||
@@ -100,13 +100,15 @@ func GetPlayer(c *gin.Context, db *sql.DB, id string) {
|
||||
}
|
||||
|
||||
func UpdatePlayer(c *gin.Context, db *sql.DB) {
|
||||
log.Println(c)
|
||||
playerID := c.Param("id")
|
||||
if playerID == "" {
|
||||
common.RespondError(c, http.StatusBadRequest, "Player ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
if playerID != c.GetString("userId") || c.GetString("role") != "admin" {
|
||||
log.Println("role: ", c.GetString("role"))
|
||||
// playerID != c.GetString("userId") ||
|
||||
if c.GetString("role") != "admin" {
|
||||
common.RespondError(c, http.StatusForbidden, "You do not have permission to update this player")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type User struct {
|
||||
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"`
|
||||
Created_at *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
|
||||
@@ -236,7 +236,7 @@ func GetAllPlayers(db *sql.DB) ([]User, error) {
|
||||
&player.lastname,
|
||||
&player.birthday,
|
||||
&player.IsActive,
|
||||
&player.createdAt,
|
||||
&player.Created_at,
|
||||
(*pq.StringArray)(&player.Role),
|
||||
); err != nil {
|
||||
log.Printf("Error scanning player row: %v", err)
|
||||
@@ -305,7 +305,7 @@ func updatePlayer(db *sql.DB, player User) error {
|
||||
|
||||
log.Printf("Updating player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email)
|
||||
|
||||
stmt := "UPDATE public.users SET name = $1, email = $2 WHERE id = $3"
|
||||
stmt := "UPDATE public.users SET username = $1, email = $2 WHERE uuid = $3"
|
||||
_, err := db.Exec(stmt, player.Username, player.Email, player.UUID)
|
||||
if err != nil {
|
||||
log.Printf("Error updating player in database: %v", err)
|
||||
|
||||
31
backend/internal/team/handler.go
Normal file
31
backend/internal/team/handler.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"volleyball/internal/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetTeams(c *gin.Context, db *sql.DB) {
|
||||
log.Println(c.GetString("userId"), c.GetString("email"), c.GetString("role"))
|
||||
// Simulate fetching players from a database
|
||||
|
||||
teams, err := GetAllTeams(db)
|
||||
if err != nil {
|
||||
log.Printf("Error retrieving teams: %v", err)
|
||||
common.RespondError(c, http.StatusInternalServerError, "Failed to retrieve players")
|
||||
return
|
||||
}
|
||||
|
||||
if len(teams) > 0 {
|
||||
log.Printf("User %s (%s) requested players", c.GetString("userId"), c.GetString("email"))
|
||||
c.JSON(http.StatusOK, teams)
|
||||
return
|
||||
}
|
||||
log.Printf("User %s (%s) requested players, but none found", c.GetString("userId"), c.GetString("email"))
|
||||
|
||||
common.RespondError(c, http.StatusNotFound, "No Players found")
|
||||
}
|
||||
25
backend/internal/team/model.go
Normal file
25
backend/internal/team/model.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"`
|
||||
Email string `db:"email" sql:"VARCHAR(255)" index:"true"`
|
||||
Username string `db:"username" sql:"VARCHAR(100)"`
|
||||
FirstName string `db:"firstname" sql:"VARCHAR(100)"`
|
||||
LastName string `db:"lastname" sql:"VARCHAR(100)"`
|
||||
}
|
||||
|
||||
type Team struct {
|
||||
UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"`
|
||||
Name string `db:"username" sql:"VARCHAR(100)"`
|
||||
Email string `db:"email" sql:"VARCHAR(255)" index:"true"`
|
||||
PlayersUUID []Player `db:"players" sql:"JSVARCHAR(255)"`
|
||||
}
|
||||
|
||||
func GetAllTeams(db *sql.DB) ([]Team, error) {
|
||||
// Implementation to retrieve all teams from the database
|
||||
return []Team{}, nil
|
||||
}
|
||||
8
frontend/src/components/interfaces/Team.tsx
Normal file
8
frontend/src/components/interfaces/Team.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import {UserBaseInformation } from "./users";
|
||||
|
||||
export interface Team {
|
||||
teamUUID: string;
|
||||
name: string;
|
||||
Player: UserBaseInformation[];
|
||||
created_at?: string; // ISO date
|
||||
}
|
||||
@@ -11,50 +11,54 @@ export enum UserRole {
|
||||
|
||||
|
||||
/** Primary user model returned from the API */
|
||||
export interface User {
|
||||
export interface UserBaseInformation {
|
||||
UUID: string
|
||||
Username: string
|
||||
AvatarUrl?: string
|
||||
IsActive: boolean
|
||||
|
||||
}
|
||||
|
||||
|
||||
export interface User extends UserBaseInformation {
|
||||
Email: string
|
||||
FirstName?: string
|
||||
LastName?: string
|
||||
AvatarUrl?: string
|
||||
Role: UserRole[]
|
||||
IsActive: boolean
|
||||
Phone?: string
|
||||
TeamId?: string[]
|
||||
CreatedAt?: string // ISO date
|
||||
UpdatedAt?: string // ISO date
|
||||
Created_at?: string // ISO date
|
||||
Updated_at?: 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[]
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// export interface AuthState {
|
||||
// currentUser?: User | null
|
||||
// token?: string | null
|
||||
// isLoading: boolean
|
||||
// error?: string | null
|
||||
// }
|
||||
10
frontend/src/components/requests/TeamData.tsx
Normal file
10
frontend/src/components/requests/TeamData.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
const API_URL = 'http://localhost:8080/api';
|
||||
|
||||
export async function getTeams(token:string)
|
||||
{
|
||||
const res = await fetch(`${API_URL}/team/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error('Fehler beim Laden des Spielers');
|
||||
return res.json();
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { fetchPlayers } from "../api";
|
||||
import { User, UserRole } from "../../components/interfaces/users";
|
||||
import UsersPage from "./Users";
|
||||
import TMP from "./TMP";
|
||||
import TeamManagement from "../Teams";
|
||||
import Teams from "./Teams";
|
||||
|
||||
// type User = {
|
||||
// id: string;
|
||||
@@ -227,36 +229,8 @@ export default function Administration(): JSX.Element {
|
||||
|
||||
{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>
|
||||
<Teams />
|
||||
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
51
frontend/src/pages/Administration/Teams.tsx
Normal file
51
frontend/src/pages/Administration/Teams.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Team } from "../../components/interfaces/Team";
|
||||
import { get } from "http";
|
||||
import { getTeams } from "../../components/requests/TeamData";
|
||||
|
||||
|
||||
|
||||
const Teams: React.FC = () => {
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Replace with API call in production
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function load()
|
||||
{
|
||||
try {
|
||||
const teams : Team[] = await getTeams(localStorage.getItem("token") || "");
|
||||
setTeams(teams);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Teams:", error);
|
||||
}
|
||||
finally {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "2rem" }}>
|
||||
<h1>Teams Administration</h1>
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: "1rem" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ borderBottom: "1px solid #ccc", textAlign: "left" }}>Team Name</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{teams.map(team => (
|
||||
<tr key={team.teamUUID}>
|
||||
<td style={{ padding: "0.5rem 0" }}>{team.name}</td>
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Teams;
|
||||
@@ -51,19 +51,25 @@ export default function UsersPage(): JSX.Element {
|
||||
params.set("page", String(page));
|
||||
params.set("limit", String(pageSize));
|
||||
|
||||
console.log("Fetching users with params:", params.toString());
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) throw new Error("No auth token found");
|
||||
const data = await fetchPlayers(token);
|
||||
|
||||
const filteredData = data.filter((user: User) =>
|
||||
user.Username.toLowerCase().includes(query.toLowerCase()) ||
|
||||
user.Email.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
setUsers(filteredData);
|
||||
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);
|
||||
// const list: User[] = Array.isArray(data) ? data : data.users ?? [];
|
||||
// setUsers(list);
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") setError(err.message || String(err));
|
||||
} finally {
|
||||
@@ -99,31 +105,21 @@ export default function UsersPage(): JSX.Element {
|
||||
const payload = { ...form };
|
||||
let res: Response;
|
||||
if (editing) {
|
||||
|
||||
console.log("Editing user:", editing.UUID, "with data:", form);
|
||||
res = await updatePlayer(editing.UUID, {
|
||||
Username: form.name,
|
||||
Email: form.email,
|
||||
Role: form.role.split(","),
|
||||
}, 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)
|
||||
@@ -176,8 +172,8 @@ export default function UsersPage(): JSX.Element {
|
||||
placeholder="Search by name or email..."
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
console.log("Search query:", e.target.value);
|
||||
setQuery(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
style={{ padding: 6, flex: 1 }}
|
||||
/>
|
||||
@@ -229,7 +225,7 @@ export default function UsersPage(): JSX.Element {
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{u.IsActive ? "Yes" : "No"}</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
{u.CreatedAt ? new Date(u.CreatedAt).toLocaleString() : "—"}
|
||||
{u.Created_at ? new Date(u.Created_at).toLocaleString() : "—"}
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
<button onClick={() => openEdit(u)} style={{ marginRight: 8 }}>
|
||||
@@ -321,11 +317,21 @@ export default function UsersPage(): JSX.Element {
|
||||
<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 }))}
|
||||
multiple
|
||||
value={form.role.split(",")}
|
||||
onChange={(e) => {
|
||||
const options = e.target.options;
|
||||
const selectedRoles: string[] = [];
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (options[i].selected) {
|
||||
selectedRoles.push(options[i].value);
|
||||
}
|
||||
}
|
||||
setForm((f) => ({ ...f, role: selectedRoles.join(",") }));
|
||||
}}
|
||||
style={{ width: "100%", padding: 8 }}
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="player">Player</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="coach">Coach</option>
|
||||
</select>
|
||||
|
||||
@@ -42,7 +42,8 @@ export default function PlayerManagement() {
|
||||
p.UUID === editingId ? { ...p, Username, Email } : p
|
||||
));
|
||||
if (token) {
|
||||
updatePlayer(editingId, { Username, Email }, token);
|
||||
// updatePlayer(editingId, { Username, Email }, token);
|
||||
throw new Error("Not implemented yet");
|
||||
}
|
||||
setEditingId(null);
|
||||
} else {
|
||||
|
||||
@@ -80,12 +80,13 @@ export async function createPlayer(player: { Username: string, Email: string },
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updatePlayer(id: string, player: { Username?: string, Email?: string }, token: string) {
|
||||
export async function updatePlayer(id: string, player: { Username?: string, Email?: string, Role: string[] }, token: string) {
|
||||
const res = await fetch(`${API_URL}/players/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(player),
|
||||
});
|
||||
console.log("Update response:", res);
|
||||
if (!res.ok) throw new Error('Spieler-Aktualisierung fehlgeschlagen');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user