From db782ff5043edf9ac826770400bc742412fe8e91 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 30 Mar 2026 21:58:30 -0700 Subject: [PATCH] feat: add player name editing functionality in admin UI and fix site_admin access to admin dashboard --- src/app/admin/page.tsx | 2 +- src/app/admin/players/page.tsx | 169 ++++++++++++++++++++---- src/app/api/admin/players/[id]/route.ts | 90 +++++++++++++ src/app/api/players/route.ts | 32 +++++ 4 files changed, 264 insertions(+), 29 deletions(-) create mode 100644 src/app/api/admin/players/[id]/route.ts create mode 100644 src/app/api/players/route.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8dfc6d3..c0987cf 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -37,7 +37,7 @@ export default async function AdminDashboard() { } // Non-admin users should see a different dashboard - if (user.role !== "club_admin") { + if (user.role !== "club_admin" && user.role !== "site_admin") { if (user.playerId) { redirect("/players/" + user.playerId + "/profile") } else { diff --git a/src/app/admin/players/page.tsx b/src/app/admin/players/page.tsx index a54e189..39c7997 100644 --- a/src/app/admin/players/page.tsx +++ b/src/app/admin/players/page.tsx @@ -1,33 +1,82 @@ -import { prisma } from "@/lib/prisma" +"use client" + +import { useState, useEffect } from "react" import Navigation from "@/components/Navigation" import Link from "next/link" -import { redirect } from "next/navigation" -import { getSession } from "@/lib/auth-simple" -import { hasRole } from "@/lib/permissions" -export default async function AdminPlayersPage() { - const session = await getSession() - - if (!session) { - redirect("/auth/login") +interface Player { + id: number + name: string + currentElo: number + gamesPlayed: number + wins: number + losses: number + user?: { + email: string + } | null +} + +export default function AdminPlayersPage() { + const [players, setPlayers] = useState([]) + const [error, setError] = useState("") + const [editingPlayer, setEditingPlayer] = useState(null) + const [newName, setNewName] = useState("") + const [isSaving, setIsSaving] = useState(false) + + useEffect(() => { + fetchPlayers() + }, []) + + const fetchPlayers = async () => { + try { + const response = await fetch("/api/players") + if (!response.ok) { + throw new Error("Failed to fetch players") + } + const data = await response.json() + setPlayers(data) + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Unknown error") + } } - // Only site_admin and club_admin can manage players - const permission = await hasRole('club_admin') - if (!permission.allowed) { - redirect("/auth/login") + const handleEditClick = (player: Player) => { + setEditingPlayer(player) + setNewName(player.name) } - // Get all players with their user association - const players = await prisma.player.findMany({ - include: { - user: true, - }, - orderBy: { name: "asc" }, - }) + const handleSaveName = async () => { + if (!editingPlayer || !newName.trim()) return - // Get total count - const playerCount = await prisma.player.count() + setIsSaving(true) + try { + const response = await fetch(`/api/admin/players/${editingPlayer.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: newName.trim() }), + }) + + const data = await response.json() + + if (data.success) { + setPlayers(players.map(p => + p.id === editingPlayer.id ? { ...p, name: data.player.name } : p + )) + setEditingPlayer(null) + setNewName("") + } else { + alert(`Error: ${data.error}`) + } + } catch (err: unknown) { + alert(`Error: ${err instanceof Error ? err.message : "Unknown error"}`) + } finally { + setIsSaving(false) + } + } + + const playerCount = players.length return (
@@ -45,6 +94,12 @@ export default async function AdminPlayersPage() {
+ {error && ( +
+ {error} +
+ )} + {/* Player Table */}
@@ -114,12 +169,20 @@ export default async function AdminPlayersPage() { )} ))} @@ -128,6 +191,56 @@ export default async function AdminPlayersPage() { + + {/* Edit Player Modal */} + {editingPlayer && ( +
+
+

+ Edit Player Name +

+ +

+ Current name: {editingPlayer.name} +

+ +
+ + setNewName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" + placeholder="Enter new player name" + /> +
+ +
+ + +
+
+
+ )} ) } diff --git a/src/app/api/admin/players/[id]/route.ts b/src/app/api/admin/players/[id]/route.ts new file mode 100644 index 0000000..0370c15 --- /dev/null +++ b/src/app/api/admin/players/[id]/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { hasRole } from "@/lib/permissions"; + +/** + * PATCH /api/admin/players/[id] + * + * Update a player's name + * Requires: club_admin or site_admin role + */ +export async function PATCH( + request: Request, + { params }: { params: { id: string } } +) { + try { + const playerId = parseInt(params.id); + + // Validate player ID + if (isNaN(playerId)) { + return NextResponse.json( + { error: "Invalid player ID" }, + { status: 400 } + ); + } + + // Check permissions - only club_admin and site_admin can edit players + const permission = await hasRole('club_admin'); + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to edit players" }, + { status: 403 } + ); + } + + // Verify player exists + const existingPlayer = await prisma.player.findUnique({ + where: { id: playerId }, + include: { user: true }, + }); + + if (!existingPlayer) { + return NextResponse.json( + { error: "Player not found" }, + { status: 404 } + ); + } + + const body = await request.json(); + const { name } = body; + + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return NextResponse.json( + { error: "Player name is required" }, + { status: 400 } + ); + } + + const trimmedName = name.trim(); + const normalizedName = trimmedName.toLowerCase(); + + // Update the player + const updatedPlayer = await prisma.player.update({ + where: { id: playerId }, + data: { + name: trimmedName, + normalizedName, + }, + }); + + // If the player has an associated user, update the user's name too + if (existingPlayer.user) { + await prisma.user.update({ + where: { id: existingPlayer.user.id }, + data: { name: trimmedName }, + }); + } + + return NextResponse.json({ + success: true, + player: updatedPlayer, + }); + } catch (error: unknown) { + console.error("Error updating player:", error); + const message = error instanceof Error ? error.message : "Failed to update player"; + return NextResponse.json( + { error: message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/players/route.ts b/src/app/api/players/route.ts new file mode 100644 index 0000000..c3cc665 --- /dev/null +++ b/src/app/api/players/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +/** + * GET /api/players + * + * Get all players with their user associations + * This is a public endpoint (no authentication required) + */ +export async function GET() { + try { + const players = await prisma.player.findMany({ + include: { + user: { + select: { + email: true, + }, + }, + }, + orderBy: { name: "asc" }, + }); + + return NextResponse.json(players); + } catch (error: unknown) { + console.error("Error fetching players:", error); + const message = error instanceof Error ? error.message : "Failed to fetch players"; + return NextResponse.json( + { error: message }, + { status: 500 } + ); + } +}
- - View Profile - +
+ + View + + +