diff --git a/src/admin/PlayerEdit.tsx b/src/admin/PlayerEdit.tsx new file mode 100644 index 000000000..74e2aa227 --- /dev/null +++ b/src/admin/PlayerEdit.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate, Link } from "react-router-dom"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui//use-toast"; + +const apiBase = import.meta.env.VITE_API_URL; + +const PlayerEdit = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const {toast} = useToast(); + + const [name, setName] = useState(""); + const [nickname, setNickname] = useState(""); + const [position, setPosition] = useState(""); + const [jerseyNumber, setJerseyNumber] = useState(); + const [birthdate, setBirthdate] = useState(""); + const [favoriteFood, setFavoriteFood] = useState(""); + const [status, setStatus] = useState("aktiv"); + const [imageUrl, setImageUrl] = useState(null); + const [uploading, setUploading] = useState(false); + + useEffect(() => { + fetchPlayer(); + }, [id]); + + const fetchPlayer = async () => { + try { + const res = await fetch(`${apiBase}/api/players/${id}`); + const data = await res.json(); + + setName(data.name || ""); + setNickname(data.nickname || ""); + setPosition(data.position || ""); + setJerseyNumber(data.jersey_number || undefined); + setBirthdate(data.birthdate ? data.birthdate.substring(0, 10) : ""); + setFavoriteFood(data.favorite_food || ""); + setStatus(data.status || "aktiv"); + setImageUrl(data.image_url ? `${apiBase}${data.image_url}` : null); + } catch (err) { + console.error("Fehler beim Laden des Spielers:", err); + } + }; + + const handleImageUpload = async (file: File) => { + const formData = new FormData(); + formData.append("image", file); + setUploading(true); + + try { + const res = await fetch(`${apiBase}/api/upload-player-image`, { + method: "POST", + body: formData, + }); + + if (!res.ok) throw new Error("Fehler beim Hochladen des Bildes"); + + const data = await res.json(); + setImageUrl(`${apiBase}${data.imageUrl}`); + } catch (err) { + console.error(err); + } finally { + setUploading(false); + } + }; + + const handleSave = async () => { + try { + const res = await fetch(`${apiBase}/api/players/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + nickname, + position, + jersey_number: jerseyNumber, + birthdate, + favorite_food: favoriteFood, + status, + image_url: imageUrl?.replace(apiBase, ""), // Nur Pfad speichern + }), + }); + + if (res.ok) { + toast({ + title: "Spieler gespeichert", + description: "Die Änderungen wurden erfolgreich übernommen.", + }); + setTimeout(() => { + navigate(-1); + }, 500); // Warte kurz, damit der Toast sichtbar bleibt + } else { + console.error("Fehler beim Speichern"); + } + } catch (err) { + console.error("Fehler beim Speichern:", err); + } + }; + + + return ( +
+

Spieler bearbeiten

+ +
+ setName(e.target.value)} /> + setNickname(e.target.value)} /> + setPosition(e.target.value)} /> + setJerseyNumber(Number(e.target.value))} + /> + setBirthdate(e.target.value)} + /> + setFavoriteFood(e.target.value)} + /> + setStatus(e.target.value)} + /> + + {/* Bild-Upload */} + { + const file = e.target.files?.[0]; + if (file) handleImageUpload(file); + }} + /> + {uploading &&

Bild wird hochgeladen...

} + {imageUrl && ( + Spielerbild + )} + + {/* Buttons */} +
+ + +
+
+
+ ); +}; + +export default PlayerEdit; diff --git a/src/admin/TeamAddPlayer.tsx b/src/admin/TeamAddPlayer.tsx new file mode 100644 index 000000000..d4eee4d97 --- /dev/null +++ b/src/admin/TeamAddPlayer.tsx @@ -0,0 +1,217 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const apiBase = import.meta.env.VITE_API_URL; + +type Player = { + id: number; + name: string; + nickname?: string; + position: string; + jersey_number?: number; + age?: number; + favorite_food?: string; + image_url?: string; +}; + +const TeamAddPlayer = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [players, setPlayers] = useState([]); + const [newPlayer, setNewPlayer] = useState({ + name: "", + nickname: "", + position: "", + jersey_number: "", + age: "", + favorite_food: "", + image_url: "", + }); + const [uploading, setUploading] = useState(false); + + const fetchPlayers = async () => { + try { + const res = await fetch(`${apiBase}/api/players`); + const data = await res.json(); + setPlayers(data); + } catch (err) { + console.error("Fehler beim Laden der Spieler:", err); + } + }; + + const handleAddExistingPlayer = async (playerId: number) => { + try { + const res = await fetch(`${apiBase}/api/teams/${id}/players`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ playerId }), + }); + + if (res.ok) { + navigate(`/admin/teams/${id}`); + } else { + console.error("Fehler beim Hinzufügen"); + } + } catch (err) { + console.error(err); + } + }; + + const handleCreateNewPlayer = async () => { + if (!newPlayer.name.trim() || !newPlayer.position.trim()) return; + + try { + const res = await fetch(`${apiBase}/api/players`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ...newPlayer, + jersey_number: newPlayer.jersey_number ? Number(newPlayer.jersey_number) : null, + age: newPlayer.age ? Number(newPlayer.age) : null, + team_ids: [Number(id)], + }), + }); + + if (res.ok) { + navigate(`/admin/teams/${id}`); + } else { + console.error("Fehler beim Erstellen des Spielers"); + } + } catch (err) { + console.error(err); + } + }; + + const handleImageUpload = async (file: File) => { + const formData = new FormData(); + formData.append("image", file); + setUploading(true); + + try { + const res = await fetch(`${apiBase}/api/upload-player-image`, { + method: "POST", + body: formData, + }); + + if (!res.ok) throw new Error("Fehler beim Bild-Upload"); + + const data = await res.json(); + setNewPlayer((prev) => ({ ...prev, image_url: data.imageUrl })); + } catch (err) { + console.error("Bild-Upload fehlgeschlagen:", err); + } finally { + setUploading(false); + } + }; + + useEffect(() => { + fetchPlayers(); + }, []); + + return ( +
+

Spieler zum Team hinzufügen

+ + {/* Bestehende Spieler */} +
+

Bestehende Spieler auswählen

+ + {players.length === 0 ? ( +

Keine Spieler gefunden.

+ ) : ( +
+ {players.map((player) => ( + + + + {player.name} {player.nickname && `(${player.nickname})`} + + + +

Position: {player.position}

+ {player.jersey_number &&

Nr: {player.jersey_number}

} + +
+
+ ))} +
+ )} +
+ + {/* Neuen Spieler anlegen */} +
+

Neuen Spieler erstellen

+
+ setNewPlayer((prev) => ({ ...prev, name: e.target.value }))} + /> + setNewPlayer((prev) => ({ ...prev, nickname: e.target.value }))} + /> + setNewPlayer((prev) => ({ ...prev, position: e.target.value }))} + /> + setNewPlayer((prev) => ({ ...prev, jersey_number: e.target.value }))} + /> + setNewPlayer((prev) => ({ ...prev, age: e.target.value }))} + /> + setNewPlayer((prev) => ({ ...prev, favorite_food: e.target.value }))} + /> + + {/* Bild Upload */} + { + const file = e.target.files?.[0]; + if (file) handleImageUpload(file); + }} + /> + {uploading &&

Bild wird hochgeladen...

} + {newPlayer.image_url && ( + Spielerbild + )} + + +
+
+
+ ); +}; + +export default TeamAddPlayer; diff --git a/src/admin/TeamCreate.tsx b/src/admin/TeamCreate.tsx new file mode 100644 index 000000000..fea5f598b --- /dev/null +++ b/src/admin/TeamCreate.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const apiBase = import.meta.env.VITE_API_URL; + +const TeamCreate = () => { + const [teamName, setTeamName] = useState(""); + const [saving, setSaving] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + + const navigate = useNavigate(); + + const handleSave = async () => { + if (!teamName.trim()) return; + + setSaving(true); + setErrorMsg(""); + + try { + const res = await fetch(`${apiBase}/api/teams`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: teamName }), + }); + + if (!res.ok) { + const msg = await res.text(); + setErrorMsg(msg); + throw new Error(msg); + } + + navigate("/admin/teams"); + } catch (err) { + console.error(err); + } finally { + setSaving(false); + } + }; + + + return ( +
+

Neues Team anlegen

+
+ setTeamName(e.target.value)} + /> + {errorMsg && ( +

+ {errorMsg} +

+ )} + +
+
+ ); +}; + +export default TeamCreate; diff --git a/src/admin/TeamDetail.tsx b/src/admin/TeamDetail.tsx new file mode 100644 index 000000000..cf82482e1 --- /dev/null +++ b/src/admin/TeamDetail.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Link } from "react-router-dom"; + +const apiBase = import.meta.env.VITE_API_URL; + +type Player = { + id: number; + name: string; + nickname?: string; + position: string; + jersey_number?: number; + image_url?: string; +}; + +const TeamDetail = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [teamName, setTeamName] = useState(""); + const [players, setPlayers] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchTeam = async () => { + try { + const res = await fetch(`${apiBase}/api/teams/${id}`); + const data = await res.json(); + setTeamName(data.name ?? ""); + setPlayers(data.players ?? []); + } catch (err) { + console.error("Fehler beim Laden des Teams:", err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTeam(); + }, [id]); + + const handleUpdateName = async () => { + try { + const res = await fetch(`${apiBase}/api/teams/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: teamName }), + }); + + if (!res.ok) throw new Error("Fehler beim Aktualisieren des Teamnamens"); + + alert("Teamname aktualisiert!"); + } catch (err) { + console.error(err); + } + }; + + const handleRemovePlayer = async (playerId: number) => { + try { + const res = await fetch(`${apiBase}/api/teams/${id}/players/${playerId}`, { + method: "DELETE", + }); + + if (res.ok) { + setPlayers((prev) => prev.filter((p) => p.id !== playerId)); + } else { + console.error("Fehler beim Entfernen des Spielers"); + } + } catch (err) { + console.error(err); + } + }; + + if (loading) return

Lade Team...

; + + return ( +
+

Team bearbeiten

+ +
+ setTeamName(e.target.value)} + placeholder="Teamname" + /> + +
+ +
+

Spieler im Team

+ + {players.length === 0 ? ( +

Noch keine Spieler zugeordnet.

+ ) : ( +
+ {players.map((player) => ( + + +
+
+ + {player.name} {player.nickname && `(${player.nickname})`} + +
+
+ {player.name} +
+
+
+ +

Position: {player.position}

+ {player.jersey_number && ( +

Nummer: {player.jersey_number}

+ )} +
+ + +
+
+
+ ))} +
+ )} +
+ +
+ +
+
+ ); + + +}; + +export default TeamDetail; diff --git a/src/admin/TeamList.tsx b/src/admin/TeamList.tsx index c012788b9..a5cd61fd7 100644 --- a/src/admin/TeamList.tsx +++ b/src/admin/TeamList.tsx @@ -13,6 +13,10 @@ type Team = { const TeamList = () => { const [teams, setTeams] = useState([]); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [teamToDelete, setTeamToDelete] = useState(null); + const [confirmInput, setConfirmInput] = useState(""); + const fetchTeams = async () => { try { @@ -24,6 +28,25 @@ const TeamList = () => { } }; + const deleteTeam = async () => { + if (!teamToDelete) return; + + try { + const res = await fetch(`${apiBase}/api/teams/${teamToDelete.id}`, { + method: "DELETE", + }); + + if (res.ok) { + setTeams((prev) => prev.filter((t) => t.id !== teamToDelete.id)); + setShowDeleteModal(false); + } else { + console.error("Fehler beim Löschen"); + } + } catch (err) { + console.error("Fehler:", err); + } + }; + useEffect(() => { fetchTeams(); }, []); @@ -36,7 +59,7 @@ const TeamList = () => { + Neues Team - +
{teams.map((team) => ( @@ -51,12 +74,61 @@ const TeamList = () => { > Bearbeiten + ))}
+ + {/* 🔥 MODAL INS RETURN */} + {showDeleteModal && teamToDelete && ( +
+
+

+ Team wirklich löschen? +

+

+ Gib den Teamnamen "{teamToDelete.name}" ein, um zu bestätigen: +

+ setConfirmInput(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2 mb-4" + placeholder="Teamname zur Bestätigung" + /> +
+ + +
+
+
+ )} ); + + + }; + + export default TeamList; diff --git a/src/main.tsx b/src/main.tsx index b5dd0325f..927a34985 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,6 +20,10 @@ import UserManagementPage from "./admin/UserManagementPage"; import UserCreatePage from "./admin/UserCreatePage"; import UserEditPage from "./admin/UserEditPage"; import TeamList from "./admin/TeamList"; +import TeamCreate from "./admin/TeamCreate"; +import TeamDetail from "./admin/TeamDetail"; +import TeamAddPlayer from "./admin/TeamAddPlayer"; +import PlayerEdit from "./admin/PlayerEdit"; @@ -61,6 +65,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render( } /> } /> } /> + } /> + } /> + } /> + } />