diff --git a/src/app/api/tournaments/[id]/teams/generate/route.ts b/src/app/api/tournaments/[id]/teams/generate/route.ts new file mode 100644 index 0000000..69ecf9b --- /dev/null +++ b/src/app/api/tournaments/[id]/teams/generate/route.ts @@ -0,0 +1,271 @@ +import { NextResponse } from "next/server" +import { prisma } from "@/lib/prisma" +import { canManageTournament } from "@/lib/permissions" + +interface RouteParams { + params: Promise<{ + id: string + }> +} + +type TeamDurability = 'permanent' | 'variable' | 'per_round' +type PartnerRotation = 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based' + +interface Team { + player1Id: number + player2Id: number + teamName: string | null +} + +/** + * POST /api/tournaments/[id]/teams/generate + * + * Generate teams for a tournament based on configuration. + * Supports multiple team generation strategies. + */ +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id } = await params + const tournamentId = parseInt(id) + + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ) + } + + const permission = await canManageTournament(tournamentId) + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to manage this tournament" }, + { status: 403 } + ) + } + + const body = await request.json() + const { teamDurability, partnerRotation, allowByes } = body as { + teamDurability: TeamDurability + partnerRotation: PartnerRotation + allowByes: boolean + } + + // Validate configuration + if (!teamDurability || !partnerRotation) { + return NextResponse.json( + { error: "teamDurability and partnerRotation are required" }, + { status: 400 } + ) + } + + // Get tournament and participants + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { + participants: { + include: { + player: true, + }, + }, + teams: true, + rounds: true, + }, + }) + + if (!tournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ) + } + + // Check if schedule exists (can't generate teams if schedule exists) + if (tournament.rounds.length > 0) { + return NextResponse.json( + { error: "Cannot generate teams after schedule has been created. Delete schedule first." }, + { status: 409 } + ) + } + + // Check participant count + const participants = tournament.participants + if (participants.length < 2) { + return NextResponse.json( + { error: "At least 2 participants are required to generate teams" }, + { status: 400 } + ) + } + + // Generate teams based on strategy + const teams = generateTeams(participants, partnerRotation, allowByes) + + // Delete existing teams + await prisma.team.deleteMany({ + where: { eventId: tournamentId }, + }) + + // Create new teams + const createdTeams = await Promise.all( + teams.map((team, index) => + prisma.team.create({ + data: { + eventId: tournamentId, + player1Id: team.player1Id, + player2Id: team.player2Id, + teamName: team.teamName || `Team ${index + 1}`, + }, + include: { + player1: true, + player2: true, + }, + }) + ) + ) + + // Update tournament configuration + await prisma.event.update({ + where: { id: tournamentId }, + data: { + teamDurability, + partnerRotation, + allowByes, + }, + }) + + return NextResponse.json({ + success: true, + teams: createdTeams, + teamsCreated: createdTeams.length, + strategy: partnerRotation, + }) + } catch (error: unknown) { + console.error("Error generating teams:", error) + const message = error instanceof Error ? error.message : "Failed to generate teams" + return NextResponse.json({ error: message }, { status: 500 }) + } +} + +/** + * Generate teams based on partner rotation strategy + */ +function generateTeams( + participants: Array<{ player: { id: number; currentElo: number; name: string } }>, + strategy: PartnerRotation, + allowByes: boolean +): Team[] { + const players = participants.map(p => p.player) + + // Handle odd number of players + if (players.length % 2 !== 0) { + if (!allowByes) { + throw new Error("Odd number of participants. Enable 'Allow Byes' to proceed.") + } + // Remove the player with the lowest ELO for bye + players.sort((a, b) => a.currentElo - b.currentElo) + players.pop() // Remove last (lowest ELO) + } + + switch (strategy) { + case 'none': // Random pairing + return generateRandomTeams(players) + + case 'minimize_repeat': + // For initial generation, we can't minimize repeats since there are no previous teams + // So we just generate random teams + return generateRandomTeams(players) + + case 'maximize_even': + // Pair players to maximize competitive balance + return generateEvenTeams(players) + + case 'elo_based': + // Pair strongest with weakest + return generateELOBasedTeams(players) + + default: + return generateRandomTeams(players) + } +} + +/** + * Generate random teams + */ +function generateRandomTeams(players: Array<{ id: number; currentElo: number; name: string }>): Team[] { + const shuffled = [...players] + + // Fisher-Yates shuffle + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]] + } + + const teams: Team[] = [] + for (let i = 0; i < shuffled.length - 1; i += 2) { + teams.push({ + player1Id: shuffled[i].id, + player2Id: shuffled[i + 1].id, + teamName: `${shuffled[i].name} & ${shuffled[i + 1].name}`, + }) + } + + return teams +} + +/** + * Generate teams to maximize competitive balance + */ +function generateEvenTeams(players: Array<{ id: number; currentElo: number; name: string }>): Team[] { + // Sort by ELO + const sorted = [...players].sort((a, b) => b.currentElo - a.currentElo) + + // Split into two halves + const midpoint = Math.floor(sorted.length / 2) + const topHalf = sorted.slice(0, midpoint) + const bottomHalf = sorted.slice(midpoint) + + const teams: Team[] = [] + + // Pair each top player with a bottom player + for (let i = 0; i < Math.min(topHalf.length, bottomHalf.length); i++) { + teams.push({ + player1Id: topHalf[i].id, + player2Id: bottomHalf[i].id, + teamName: `${topHalf[i].name} & ${bottomHalf[i].name}`, + }) + } + + // Handle odd number of teams + if (topHalf.length > bottomHalf.length) { + teams.push({ + player1Id: topHalf[topHalf.length - 1].id, + player2Id: topHalf[topHalf.length - 2].id, + teamName: `${topHalf[topHalf.length - 1].name} & ${topHalf[topHalf.length - 2].name}`, + }) + } + + return teams +} + +/** + * Generate ELO-based teams (strongest + weakest) + */ +function generateELOBasedTeams(players: Array<{ id: number; currentElo: number; name: string }>): Team[] { + // Sort by ELO + const sorted = [...players].sort((a, b) => b.currentElo - a.currentElo) + + const teams: Team[] = [] + + // Pair strongest with weakest + for (let i = 0; i < sorted.length / 2; i++) { + const j = sorted.length - 1 - i + if (i >= j) break + + teams.push({ + player1Id: sorted[i].id, + player2Id: sorted[j].id, + teamName: `${sorted[i].name} & ${sorted[j].name}`, + }) + } + + return teams +} diff --git a/src/app/api/tournaments/[id]/teams/route.ts b/src/app/api/tournaments/[id]/teams/route.ts new file mode 100644 index 0000000..f05b3df --- /dev/null +++ b/src/app/api/tournaments/[id]/teams/route.ts @@ -0,0 +1,135 @@ +import { NextResponse } from "next/server" +import { prisma } from "@/lib/prisma" +import { canManageTournament } from "@/lib/permissions" + +interface RouteParams { + params: Promise<{ + id: string + }> +} + +/** + * DELETE /api/tournaments/[id]/teams + * + * Delete all teams for a tournament. + * This will also delete any associated schedule. + */ +export async function DELETE(_request: Request, { params }: RouteParams) { + try { + const { id } = await params + const tournamentId = parseInt(id) + + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ) + } + + const permission = await canManageTournament(tournamentId) + if (!permission.allowed) { + return NextResponse.json( + { error: permission.reason || "Not authorized to manage this tournament" }, + { status: 403 } + ) + } + + // Check if tournament exists + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { + teams: true, + rounds: true, + }, + }) + + if (!tournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ) + } + + // Check if schedule exists + if (tournament.rounds.length > 0) { + return NextResponse.json( + { error: "Cannot delete teams while schedule exists. Delete schedule first." }, + { status: 409 } + ) + } + + // Delete all teams + const deletedTeams = await prisma.team.deleteMany({ + where: { eventId: tournamentId }, + }) + + // Reset team configuration + await prisma.event.update({ + where: { id: tournamentId }, + data: { + teamDurability: "permanent", + partnerRotation: "none", + allowByes: true, + }, + }) + + return NextResponse.json({ + success: true, + deletedTeams: deletedTeams.count, + }) + } catch (error: unknown) { + console.error("Error deleting teams:", error) + const message = error instanceof Error ? error.message : "Failed to delete teams" + return NextResponse.json({ error: message }, { status: 500 }) + } +} + +/** + * GET /api/tournaments/[id]/teams + * + * Get all teams for a tournament. + */ +export async function GET(_request: Request, { params }: RouteParams) { + try { + const { id } = await params + const tournamentId = parseInt(id) + + if (isNaN(tournamentId)) { + return NextResponse.json( + { error: "Invalid tournament ID" }, + { status: 400 } + ) + } + + // Get tournament and teams + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { + teams: { + include: { + player1: true, + player2: true, + }, + }, + }, + }) + + if (!tournament) { + return NextResponse.json( + { error: "Tournament not found" }, + { status: 404 } + ) + } + + return NextResponse.json({ + teams: tournament.teams, + teamDurability: tournament.teamDurability, + partnerRotation: tournament.partnerRotation, + allowByes: tournament.allowByes, + }) + } catch (error: unknown) { + console.error("Error fetching teams:", error) + const message = error instanceof Error ? error.message : "Failed to fetch teams" + return NextResponse.json({ error: message }, { status: 500 }) + } +}