4d685558aa
- Change PUT endpoint to only include fields present in the request - Add support for team configuration fields (teamDurability, partnerRotation, allowByes) - Fix contradictory test that expected resetting allowTies when not provided - When a field is not in the request, it is preserved (not modified) Fixes issue where Save Configuration button would fail with 'Tournament name is required' error.
334 lines
9.6 KiB
TypeScript
334 lines
9.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { canManageTournament, canDeleteTournament } from "@/lib/permissions";
|
|
import { getTournamentStatus } from "@/lib/tournamentUtils";
|
|
|
|
interface RouteParams {
|
|
params: Promise<{
|
|
id: string;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* GET /api/tournaments/[id]
|
|
*
|
|
* Get a single tournament by ID
|
|
*/
|
|
export async function GET(request: Request, { params }: RouteParams) {
|
|
try {
|
|
const { id } = await params
|
|
const tournamentId = parseInt(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 { id } = await params
|
|
const tournamentId = parseInt(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,
|
|
tournamentType,
|
|
format,
|
|
status,
|
|
maxParticipants,
|
|
ownerId,
|
|
targetScore,
|
|
allowTies,
|
|
teamDurability,
|
|
partnerRotation,
|
|
allowByes,
|
|
} = body;
|
|
|
|
// Validate name only if it's being updated
|
|
if (body.hasOwnProperty('name')) {
|
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "Tournament name is required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Prepare update data - only include fields that are present in the request
|
|
const updateData: Record<string, unknown> = {};
|
|
|
|
if (body.hasOwnProperty('name')) updateData.name = name.trim();
|
|
if (body.hasOwnProperty('description')) updateData.description = description || null;
|
|
if (body.hasOwnProperty('eventDate')) updateData.eventDate = eventDate ? new Date(eventDate) : null;
|
|
if (body.hasOwnProperty('eventType')) updateData.eventType = eventType || "tournament";
|
|
if (body.hasOwnProperty('tournamentType')) updateData.tournamentType = tournamentType || "individual";
|
|
if (body.hasOwnProperty('format')) updateData.format = format || "round_robin";
|
|
if (body.hasOwnProperty('maxParticipants')) updateData.maxParticipants = maxParticipants ? parseInt(maxParticipants) : null;
|
|
if (body.hasOwnProperty('targetScore')) updateData.targetScore = targetScore ? parseInt(targetScore) : null;
|
|
if (body.hasOwnProperty('allowTies')) updateData.allowTies = allowTies ?? false;
|
|
if (body.hasOwnProperty('teamDurability')) updateData.teamDurability = teamDurability || "permanent";
|
|
if (body.hasOwnProperty('partnerRotation')) updateData.partnerRotation = partnerRotation || "none";
|
|
if (body.hasOwnProperty('allowByes')) updateData.allowByes = allowByes ?? true;
|
|
|
|
// 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 { id } = await params
|
|
const tournamentId = parseInt(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 }
|
|
);
|
|
}
|
|
}
|