diff --git a/src/app/api/admin/tournaments/[id]/route.ts b/src/app/api/admin/tournaments/[id]/route.ts deleted file mode 100644 index 7ca9c7e..0000000 --- a/src/app/api/admin/tournaments/[id]/route.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; -import { canDeleteTournament } from "@/lib/permissions"; - -/** - * DELETE /api/admin/tournaments/[id] - * - * Delete a tournament and optionally its associated matches - * Options: - * - deleteMatches: Delete all matches associated with the tournament - * - orphanMatches: Keep matches but remove tournament association (eventId becomes null) - */ -export async function DELETE( - request: Request, - { params }: { params: { id: string } } -) { - try { - const tournamentId = parseInt(params.id); - const body = await request.json(); - const { deleteMatches = false } = body; - - // Validate tournament ID - if (isNaN(tournamentId)) { - return NextResponse.json( - { error: "Invalid tournament ID" }, - { status: 400 } - ); - } - - // Check if user has permission to delete this tournament - const permission = await canDeleteTournament(tournamentId); - if (!permission.allowed) { - return NextResponse.json( - { error: permission.reason || "Not authorized to delete this tournament" }, - { status: 403 } - ); - } - - // Verify tournament exists - const tournament = await prisma.event.findUnique({ - where: { id: tournamentId }, - include: { matches: true }, - }); - - if (!tournament) { - return NextResponse.json( - { error: "Tournament not found" }, - { status: 404 } - ); - } - - const matchCount = tournament.matches.length; - - // Handle matches based on user choice - if (deleteMatches) { - // Delete all matches associated with the tournament - await prisma.match.deleteMany({ - where: { eventId: tournamentId }, - }); - } else { - // Orphan matches - remove tournament association - await prisma.match.updateMany({ - where: { eventId: tournamentId }, - data: { eventId: null }, - }); - } - - // Delete event participants - await prisma.eventParticipant.deleteMany({ - where: { eventId: tournamentId }, - }); - - // Delete teams - await prisma.team.deleteMany({ - where: { eventId: tournamentId }, - }); - - // Delete tournament rounds - await prisma.tournamentRound.deleteMany({ - where: { eventId: tournamentId }, - }); - - // Delete bracket matchups - await prisma.bracketMatchup.deleteMany({ - where: { eventId: tournamentId }, - }); - - // Delete the tournament itself - await prisma.event.delete({ - where: { id: tournamentId }, - }); - - return NextResponse.json({ - success: true, - message: "Tournament deleted successfully", - deletedTournament: tournament.name, - deletedMatches: deleteMatches ? matchCount : 0, - orphanedMatches: deleteMatches ? 0 : matchCount, - }); - } catch (error: unknown) { - console.error("Error deleting tournament:", error); - const message = error instanceof Error ? error.message : "Failed to delete tournament"; - return NextResponse.json( - { error: message }, - { status: 500 } - ); - } -} diff --git a/src/app/api/tournaments/[id]/route.ts b/src/app/api/tournaments/[id]/route.ts new file mode 100644 index 0000000..e7cb42b --- /dev/null +++ b/src/app/api/tournaments/[id]/route.ts @@ -0,0 +1,316 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { canManageTournament, canDeleteTournament } from "@/lib/permissions"; +import { getTournamentStatus } from "@/lib/tournamentUtils"; + +interface RouteParams { + params: { + id: string; + }; +} + +/** + * GET /api/tournaments/[id] + * + * Get a single tournament by ID + */ +export async function GET(request: Request, { params }: RouteParams) { + try { + const tournamentId = parseInt(params.id); + + // Validate tournament ID + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ); + } + + // Check if user can view/manage this tournament + const permission = await canManageTournament(tournamentId); + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to view this tournament" }, + { status: 403 } + ); + } + + // Fetch the tournament + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { + participants: { + include: { + player: true, + }, + }, + teams: { + include: { + player1: true, + player2: true, + }, + }, + rounds: { + include: { + bracketMatchups: { + include: { + team1: { + include: { + player1: true, + player2: true, + }, + }, + team2: { + include: { + player1: true, + player2: true, + }, + }, + match: true, + }, + }, + }, + }, + }, + }); + + if (!tournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ); + } + + // Update tournament status if needed + const calculatedStatus = getTournamentStatus(tournament.eventDate); + if (tournament.status !== calculatedStatus) { + tournament.status = calculatedStatus; + } + + return NextResponse.json({ tournament }); + } catch (error: unknown) { + console.error("Error fetching tournament:", error); + const message = error instanceof Error ? error.message : "Failed to fetch tournament"; + return NextResponse.json( + { error: message }, + { status: 500 } + ); + } +} + +/** + * PUT /api/tournaments/[id] + * + * Update a tournament by ID + */ +export async function PUT(request: Request, { params }: RouteParams) { + try { + const tournamentId = parseInt(params.id); + + // Validate tournament ID + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ); + } + + // Check if user can manage this tournament + const permission = await canManageTournament(tournamentId); + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to update this tournament" }, + { status: 403 } + ); + } + + // Verify tournament exists + const existingTournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + }); + + if (!existingTournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ); + } + + const body = await request.json(); + const { + name, + description, + eventDate, + eventType, + format, + status, + maxParticipants, + ownerId, + } = body; + + // Validate required fields + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return NextResponse.json( + { error: "Tournament name is required" }, + { status: 400 } + ); + } + + // Prepare update data + const updateData: Record = { + name: name.trim(), + description: description || null, + eventDate: eventDate ? new Date(eventDate) : null, + eventType: eventType || "tournament", + format: format || "round_robin", + maxParticipants: maxParticipants ? parseInt(maxParticipants) : null, + }; + + // Only allow status updates if they don't conflict with auto-calculation + if (status) { + const calculatedStatus = getTournamentStatus(eventDate ? new Date(eventDate) : existingTournament.eventDate); + if (status !== calculatedStatus) { + // Allow manual status override but log it + console.log(`Manual status override: ${existingTournament.status} -> ${status} (calculated: ${calculatedStatus})`); + } + updateData.status = status; + } + + // Update owner if specified (and user has permission to reassign ownership) + if (ownerId !== undefined) { + // Only site_admin or club_admin can reassign ownership + const canReassign = await canDeleteTournament(tournamentId); + if (canReassign.allowed) { + updateData.ownerId = ownerId || null; + } + } + + // Update the tournament + const updatedTournament = await prisma.event.update({ + where: { id: tournamentId }, + data: updateData, + }); + + return NextResponse.json({ + success: true, + tournament: updatedTournament, + }); + } catch (error: unknown) { + console.error("Error updating tournament:", error); + const message = error instanceof Error ? error.message : "Failed to update tournament"; + return NextResponse.json( + { error: message }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/tournaments/[id] + * + * Delete a tournament by ID + * Options: + * - deleteMatches: Delete all matches associated with the tournament + * - orphanMatches: Keep matches but remove tournament association (eventId becomes null) + */ +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const tournamentId = parseInt(params.id); + + // Validate tournament ID + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ); + } + + // Check if user has permission to delete this tournament + const permission = await canDeleteTournament(tournamentId); + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to delete this tournament" }, + { status: 403 } + ); + } + + // Verify tournament exists + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { matches: true }, + }); + + if (!tournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ); + } + + // Parse request body to get delete options + let deleteMatches = false; + try { + const body = await request.json(); + deleteMatches = body.deleteMatches || false; + } catch { + // If no body is provided, default to orphaning matches + deleteMatches = false; + } + + const matchCount = tournament.matches.length; + + // Handle matches based on user choice + if (deleteMatches) { + // Delete all matches associated with the tournament + await prisma.match.deleteMany({ + where: { eventId: tournamentId }, + }); + } else { + // Orphan matches - remove tournament association + await prisma.match.updateMany({ + where: { eventId: tournamentId }, + data: { eventId: null }, + }); + } + + // Delete event participants + await prisma.eventParticipant.deleteMany({ + where: { eventId: tournamentId }, + }); + + // Delete teams + await prisma.team.deleteMany({ + where: { eventId: tournamentId }, + }); + + // Delete tournament rounds + await prisma.tournamentRound.deleteMany({ + where: { eventId: tournamentId }, + }); + + // Delete bracket matchups + await prisma.bracketMatchup.deleteMany({ + where: { eventId: tournamentId }, + }); + + // Delete the tournament itself + await prisma.event.delete({ + where: { id: tournamentId }, + }); + + return NextResponse.json({ + success: true, + message: "Tournament deleted successfully", + deletedTournament: tournament.name, + deletedMatches: deleteMatches ? matchCount : 0, + orphanedMatches: deleteMatches ? 0 : matchCount, + }); + } catch (error: unknown) { + console.error("Error deleting tournament:", error); + const message = error instanceof Error ? error.message : "Failed to delete tournament"; + return NextResponse.json( + { error: message }, + { status: 500 } + ); + } +} diff --git a/src/components/DeleteTournamentButton.tsx b/src/components/DeleteTournamentButton.tsx index cd2542e..512ecd3 100644 --- a/src/components/DeleteTournamentButton.tsx +++ b/src/components/DeleteTournamentButton.tsx @@ -16,7 +16,7 @@ export function DeleteTournamentButton({ tournamentId, tournamentName, matchCoun const handleDelete = async () => { setIsDeleting(true) try { - const response = await fetch(`/api/admin/tournaments/${tournamentId}`, { + const response = await fetch(`/api/tournaments/${tournamentId}`, { method: "DELETE", headers: { "Content-Type": "application/json",