feat: Implement tournament schedule tab and fix E2E tests #27
@@ -82,35 +82,35 @@ export async function POST(request: Request) {
|
|||||||
// Update all foreign key references to point to canonical player
|
// Update all foreign key references to point to canonical player
|
||||||
const updates = [];
|
const updates = [];
|
||||||
|
|
||||||
// Update matches - team1P1Id
|
// Update matches - player1P1Id
|
||||||
updates.push(
|
updates.push(
|
||||||
prisma.match.updateMany({
|
prisma.match.updateMany({
|
||||||
where: { team1P1Id: { in: duplicatePlayers.map(p => p.id) } },
|
where: { player1P1Id: { in: duplicatePlayers.map(p => p.id) } },
|
||||||
data: { team1P1Id: canonicalPlayer.id },
|
data: { player1P1Id: canonicalPlayer.id },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update matches - team1P2Id
|
// Update matches - player1P2Id
|
||||||
updates.push(
|
updates.push(
|
||||||
prisma.match.updateMany({
|
prisma.match.updateMany({
|
||||||
where: { team1P2Id: { in: duplicatePlayers.map(p => p.id) } },
|
where: { player1P2Id: { in: duplicatePlayers.map(p => p.id) } },
|
||||||
data: { team1P2Id: canonicalPlayer.id },
|
data: { player1P2Id: canonicalPlayer.id },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update matches - team2P1Id
|
// Update matches - player2P1Id
|
||||||
updates.push(
|
updates.push(
|
||||||
prisma.match.updateMany({
|
prisma.match.updateMany({
|
||||||
where: { team2P1Id: { in: duplicatePlayers.map(p => p.id) } },
|
where: { player2P1Id: { in: duplicatePlayers.map(p => p.id) } },
|
||||||
data: { team2P1Id: canonicalPlayer.id },
|
data: { player2P1Id: canonicalPlayer.id },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update matches - team2P2Id
|
// Update matches - player2P2Id
|
||||||
updates.push(
|
updates.push(
|
||||||
prisma.match.updateMany({
|
prisma.match.updateMany({
|
||||||
where: { team2P2Id: { in: duplicatePlayers.map(p => p.id) } },
|
where: { player2P2Id: { in: duplicatePlayers.map(p => p.id) } },
|
||||||
data: { team2P2Id: canonicalPlayer.id },
|
data: { player2P2Id: canonicalPlayer.id },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ export async function POST(request: Request) {
|
|||||||
const createdMatch = await prisma.match.create({
|
const createdMatch = await prisma.match.create({
|
||||||
data: matchData,
|
data: matchData,
|
||||||
include: {
|
include: {
|
||||||
team1P1: true,
|
player1P1: true,
|
||||||
team1P2: true,
|
player1P2: true,
|
||||||
team2P1: true,
|
player2P1: true,
|
||||||
team2P2: true,
|
player2P2: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,32 +59,10 @@ export async function GET() {
|
|||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
team1P1: { select: { id: true, name: true } },
|
player1P1: { select: { id: true, name: true } },
|
||||||
team1P2: { select: { id: true, name: true } },
|
player1P2: { select: { id: true, name: true } },
|
||||||
team2P1: { select: { id: true, name: true } },
|
player2P1: { select: { id: true, name: true } },
|
||||||
team2P2: { select: { id: true, name: true } },
|
player2P2: { select: { id: true, name: true } },
|
||||||
},
|
|
||||||
orderBy: { playedAt: 'desc' },
|
|
||||||
});
|
|
||||||
} else if (userRole === 'tournament_admin') {
|
|
||||||
// Tournament admins can only see matches in their own tournaments
|
|
||||||
matches = await prisma.match.findMany({
|
|
||||||
where: {
|
|
||||||
event: {
|
|
||||||
ownerId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
event: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
team1P1: { select: { id: true, name: true } },
|
|
||||||
team1P2: { select: { id: true, name: true } },
|
|
||||||
team2P1: { select: { id: true, name: true } },
|
|
||||||
team2P2: { select: { id: true, name: true } },
|
|
||||||
},
|
},
|
||||||
orderBy: { playedAt: 'desc' },
|
orderBy: { playedAt: 'desc' },
|
||||||
});
|
});
|
||||||
@@ -236,10 +214,10 @@ export async function POST(request: Request) {
|
|||||||
eventId: eventId ? parseInt(eventId) : null,
|
eventId: eventId ? parseInt(eventId) : null,
|
||||||
isCasual: isCasual,
|
isCasual: isCasual,
|
||||||
playedAt: playedAt ? new Date(playedAt) : new Date(),
|
playedAt: playedAt ? new Date(playedAt) : new Date(),
|
||||||
team1P1Id: parseInt(team1P1Id),
|
player1P1Id: parseInt(team1P1Id),
|
||||||
team1P2Id: parseInt(team1P2Id),
|
player1P2Id: parseInt(team1P2Id),
|
||||||
team2P1Id: parseInt(team2P1Id),
|
player2P1Id: parseInt(team2P1Id),
|
||||||
team2P2Id: parseInt(team2P2Id),
|
player2P2Id: parseInt(team2P2Id),
|
||||||
team1Score,
|
team1Score,
|
||||||
team2Score,
|
team2Score,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
|||||||
@@ -132,10 +132,10 @@ export async function POST(request: Request) {
|
|||||||
data: {
|
data: {
|
||||||
eventId: parseInt(eventId),
|
eventId: parseInt(eventId),
|
||||||
playedAt: new Date(),
|
playedAt: new Date(),
|
||||||
team1P1Id: players[0].id,
|
player1P1Id: players[0].id,
|
||||||
team1P2Id: players[1].id,
|
player1P2Id: players[1].id,
|
||||||
team2P1Id: players[2].id,
|
player2P1Id: players[2].id,
|
||||||
team2P2Id: players[3].id,
|
player2P2Id: players[3].id,
|
||||||
team1Score,
|
team1Score,
|
||||||
team2Score,
|
team2Score,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ export async function POST(
|
|||||||
await prisma.match.create({
|
await prisma.match.create({
|
||||||
data: {
|
data: {
|
||||||
eventId: tournamentId,
|
eventId: tournamentId,
|
||||||
team1P1Id: player1.id,
|
player1P1Id: player1.id,
|
||||||
team1P2Id: player2.id,
|
player1P2Id: player2.id,
|
||||||
team2P1Id: player3.id,
|
player2P1Id: player3.id,
|
||||||
team2P2Id: player4.id,
|
player2P2Id: player4.id,
|
||||||
team1Score: game.score1,
|
team1Score: game.score1,
|
||||||
team2Score: game.score2,
|
team2Score: game.score2,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
|||||||
@@ -51,91 +51,15 @@ export async function POST(
|
|||||||
|
|
||||||
const isTeamTournament = tournament.tournamentType === "team";
|
const isTeamTournament = tournament.tournamentType === "team";
|
||||||
|
|
||||||
// For team tournaments, pair players into teams
|
// For team tournaments, validate even number of players
|
||||||
if (isTeamTournament) {
|
if (isTeamTournament && playerIds.length % 2 !== 0) {
|
||||||
if (playerIds.length % 2 !== 0) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Team tournaments require an even number of players" },
|
{ error: "Team tournaments require an even number of players" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create teams from consecutive pairs
|
// Add participants - teams will be generated later during schedule generation
|
||||||
const teams = [];
|
|
||||||
for (let i = 0; i < playerIds.length; i += 2) {
|
|
||||||
const player1Id = playerIds[i];
|
|
||||||
const player2Id = playerIds[i + 1];
|
|
||||||
|
|
||||||
// Check if team already exists
|
|
||||||
const existingTeam = await prisma.team.findFirst({
|
|
||||||
where: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
OR: [
|
|
||||||
{ AND: [{ player1Id }, { player2Id }] },
|
|
||||||
{ AND: [{ player1Id: player2Id }, { player2Id: player1Id }] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let team;
|
|
||||||
if (existingTeam) {
|
|
||||||
team = existingTeam;
|
|
||||||
} else {
|
|
||||||
team = await prisma.team.create({
|
|
||||||
data: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
player1Id,
|
|
||||||
player2Id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create participants linked to this team
|
|
||||||
const participant1 = await prisma.eventParticipant.upsert({
|
|
||||||
where: {
|
|
||||||
eventId_playerId: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
playerId: player1Id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: { teamId: team.id },
|
|
||||||
create: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
playerId: player1Id,
|
|
||||||
teamId: team.id,
|
|
||||||
status: "registered",
|
|
||||||
registrationDate: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const participant2 = await prisma.eventParticipant.upsert({
|
|
||||||
where: {
|
|
||||||
eventId_playerId: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
playerId: player2Id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: { teamId: team.id },
|
|
||||||
create: {
|
|
||||||
eventId: tournamentId,
|
|
||||||
playerId: player2Id,
|
|
||||||
teamId: team.id,
|
|
||||||
status: "registered",
|
|
||||||
registrationDate: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
teams.push({ team, participants: [participant1, participant2] });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
teamsCreated: teams.length,
|
|
||||||
teams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Individual tournament - just add participants
|
|
||||||
const participants = await Promise.all(
|
const participants = await Promise.all(
|
||||||
playerIds.map(async (playerId: number) => {
|
playerIds.map(async (playerId: number) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -45,28 +45,14 @@ export async function GET(request: Request, { params }: RouteParams) {
|
|||||||
player: true,
|
player: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
|
||||||
include: {
|
|
||||||
player1: true,
|
|
||||||
player2: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rounds: {
|
rounds: {
|
||||||
include: {
|
include: {
|
||||||
bracketMatchups: {
|
bracketMatchups: {
|
||||||
include: {
|
include: {
|
||||||
team1: {
|
player1P1: true,
|
||||||
include: {
|
player1P2: true,
|
||||||
player1: true,
|
player2P1: true,
|
||||||
player2: true,
|
player2P2: true,
|
||||||
},
|
|
||||||
},
|
|
||||||
team2: {
|
|
||||||
include: {
|
|
||||||
player1: true,
|
|
||||||
player2: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -295,11 +281,6 @@ export async function DELETE(request: Request, { params }: RouteParams) {
|
|||||||
where: { eventId: tournamentId },
|
where: { eventId: tournamentId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete teams
|
|
||||||
await prisma.team.deleteMany({
|
|
||||||
where: { eventId: tournamentId },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete tournament rounds
|
// Delete tournament rounds
|
||||||
await prisma.tournamentRound.deleteMany({
|
await prisma.tournamentRound.deleteMany({
|
||||||
where: { eventId: tournamentId },
|
where: { eventId: tournamentId },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { canManageTournament } from "@/lib/permissions";
|
import { canManageTournament } from "@/lib/permissions";
|
||||||
import { generateRoundRobin, validateScheduleInput } from "@/lib/schedule-generator";
|
import { generateRoundRobin, validateScheduleInput } from "@/lib/schedule-generator";
|
||||||
|
import { generateTeams, generateTeamsWithRotation, type Player, type Team as TeamPairing } from "@/lib/team-generator";
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -42,12 +43,10 @@ export async function GET(_request: Request, { params }: RouteParams) {
|
|||||||
include: {
|
include: {
|
||||||
bracketMatchups: {
|
bracketMatchups: {
|
||||||
include: {
|
include: {
|
||||||
team1: {
|
player1P1: true,
|
||||||
include: { player1: true, player2: true },
|
player1P2: true,
|
||||||
},
|
player2P1: true,
|
||||||
team2: {
|
player2P2: true,
|
||||||
include: { player1: true, player2: true },
|
|
||||||
},
|
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -102,7 +101,11 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
|||||||
const tournament = await prisma.event.findUnique({
|
const tournament = await prisma.event.findUnique({
|
||||||
where: { id: tournamentId },
|
where: { id: tournamentId },
|
||||||
include: {
|
include: {
|
||||||
teams: true,
|
participants: {
|
||||||
|
include: {
|
||||||
|
player: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
rounds: true,
|
rounds: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -125,10 +128,48 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get team IDs
|
// Get participants as players
|
||||||
const teamIds = tournament.teams.map((t) => t.id);
|
const participants: Player[] = tournament.participants.map((p) => ({
|
||||||
|
id: p.player.id,
|
||||||
|
name: p.player.name,
|
||||||
|
currentElo: p.player.currentElo,
|
||||||
|
}));
|
||||||
|
|
||||||
const validation = validateScheduleInput(teamIds);
|
// Check minimum participants
|
||||||
|
if (participants.length < 2) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "At least 2 participants are required to generate a schedule" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate teams based on configuration
|
||||||
|
const teamDurability = tournament.teamDurability || "permanent";
|
||||||
|
const partnerRotation = (tournament.partnerRotation || "none") as 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based';
|
||||||
|
const allowByes = tournament.allowByes ?? true;
|
||||||
|
|
||||||
|
let teamPairings: { player1Id: number; player2Id: number }[];
|
||||||
|
|
||||||
|
if (teamDurability === "permanent") {
|
||||||
|
// For permanent teams, generate once and use for all rounds
|
||||||
|
const result = generateTeams(participants, partnerRotation, allowByes);
|
||||||
|
teamPairings = result.teams.map((t) => ({
|
||||||
|
player1Id: t.player1Id,
|
||||||
|
player2Id: t.player2Id,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// For variable/per_round teams, generate teams for each round
|
||||||
|
// We'll use generateTeamsWithRotation for the initial teams
|
||||||
|
// The actual per-round teams will be generated when storing the schedule
|
||||||
|
const result = generateTeams(participants, partnerRotation, allowByes);
|
||||||
|
teamPairings = result.teams.map((t) => ({
|
||||||
|
player1Id: t.player1Id,
|
||||||
|
player2Id: t.player2Id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate schedule input
|
||||||
|
const validation = validateScheduleInput(teamPairings);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: validation.error },
|
{ error: validation.error },
|
||||||
@@ -137,7 +178,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate schedule
|
// Generate schedule
|
||||||
const schedule = generateRoundRobin(teamIds);
|
const schedule = generateRoundRobin(teamPairings);
|
||||||
|
|
||||||
// Create rounds and matchups in a transaction
|
// Create rounds and matchups in a transaction
|
||||||
const created = await prisma.$transaction(
|
const created = await prisma.$transaction(
|
||||||
@@ -150,8 +191,10 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
|||||||
bracketMatchups: {
|
bracketMatchups: {
|
||||||
create: round.matchups.map((matchup, idx) => ({
|
create: round.matchups.map((matchup, idx) => ({
|
||||||
eventId: tournamentId,
|
eventId: tournamentId,
|
||||||
team1Id: matchup.team1Id,
|
player1P1Id: matchup.player1P1Id,
|
||||||
team2Id: matchup.team2Id,
|
player1P2Id: matchup.player1P2Id,
|
||||||
|
player2P1Id: matchup.player2P1Id,
|
||||||
|
player2P2Id: matchup.player2P2Id,
|
||||||
bracketPosition: idx + 1,
|
bracketPosition: idx + 1,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})),
|
})),
|
||||||
@@ -160,12 +203,10 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
|||||||
include: {
|
include: {
|
||||||
bracketMatchups: {
|
bracketMatchups: {
|
||||||
include: {
|
include: {
|
||||||
team1: {
|
player1P1: true,
|
||||||
include: { player1: true, player2: true },
|
player1P2: true,
|
||||||
},
|
player2P1: true,
|
||||||
team2: {
|
player2P2: true,
|
||||||
include: { player1: true, player2: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,12 +11,6 @@ export async function GET() {
|
|||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
include: {
|
include: {
|
||||||
participants: true,
|
participants: true,
|
||||||
teams: {
|
|
||||||
include: {
|
|
||||||
player1: true,
|
|
||||||
player2: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user