fix: add tournament update API endpoint and consolidate delete into main tournaments API
This commit is contained in:
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, unknown> = {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export function DeleteTournamentButton({ tournamentId, tournamentName, matchCoun
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setIsDeleting(true)
|
setIsDeleting(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/admin/tournaments/${tournamentId}`, {
|
const response = await fetch(`/api/tournaments/${tournamentId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
Reference in New Issue
Block a user