feat: add tournament creation wizard with participant management and bulk game entry
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { canManageTournament } from "@/lib/permissions";
|
||||
import { getSession } from "@/lib/auth-simple";
|
||||
import { calculateTeamElo, calculateTeamEloChange } from "@/lib/elo-utils";
|
||||
|
||||
interface GameEntry {
|
||||
round: number
|
||||
table: string
|
||||
player1: string
|
||||
player2: string
|
||||
score1: number
|
||||
player3: string
|
||||
player4: string
|
||||
score2: number
|
||||
}
|
||||
|
||||
const K_FACTOR = 32;
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const tournamentId = parseInt(params.id);
|
||||
const body = await request.json();
|
||||
const { games } = body;
|
||||
|
||||
if (!games || !Array.isArray(games) || games.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "games array is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
const permission = await canManageTournament(tournamentId);
|
||||
if (!permission.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: permission.reason || 'Insufficient permissions' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get current user
|
||||
const session = await getSession();
|
||||
const userId = session?.user?.id || null;
|
||||
|
||||
let importedCount = 0;
|
||||
let errorCount = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
// Process each game
|
||||
for (const [index, game] of games.entries()) {
|
||||
try {
|
||||
// Find players by name (case-insensitive partial match)
|
||||
const [player1, player2, player3, player4] = await Promise.all([
|
||||
findPlayerByName(game.player1),
|
||||
findPlayerByName(game.player2),
|
||||
findPlayerByName(game.player3),
|
||||
findPlayerByName(game.player4),
|
||||
]);
|
||||
|
||||
// Check if all players were found
|
||||
if (!player1 || !player2 || !player3 || !player4) {
|
||||
const missing = [];
|
||||
if (!player1) missing.push(game.player1);
|
||||
if (!player2) missing.push(game.player2);
|
||||
if (!player3) missing.push(game.player3);
|
||||
if (!player4) missing.push(game.player4);
|
||||
errors.push(`Game ${index + 1}: Player not found: ${missing.join(", ")}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate Elo changes
|
||||
const team1Rating = calculateTeamElo(player1.currentElo, player2.currentElo);
|
||||
const team2Rating = calculateTeamElo(player3.currentElo, player4.currentElo);
|
||||
|
||||
const team1Won = game.score1 > game.score2;
|
||||
const team2Won = game.score2 > game.score1;
|
||||
const isTie = game.score1 === game.score2;
|
||||
|
||||
const team1ScoreActual = isTie ? 0.5 : (team1Won ? 1 : 0);
|
||||
const team2ScoreActual = isTie ? 0.5 : (team2Won ? 1 : 0);
|
||||
|
||||
const team1EloChange = calculateTeamEloChange(team1Rating, team2Rating, team1ScoreActual, K_FACTOR);
|
||||
const team2EloChange = calculateTeamEloChange(team2Rating, team1Rating, team2ScoreActual, K_FACTOR);
|
||||
|
||||
const player1EloChange = team1EloChange / 2;
|
||||
const player2EloChange = team1EloChange / 2;
|
||||
const player3EloChange = team2EloChange / 2;
|
||||
const player4EloChange = team2EloChange / 2;
|
||||
|
||||
// Create match
|
||||
await prisma.match.create({
|
||||
data: {
|
||||
eventId: tournamentId,
|
||||
team1P1Id: player1.id,
|
||||
team1P2Id: player2.id,
|
||||
team2P1Id: player3.id,
|
||||
team2P2Id: player4.id,
|
||||
team1Score: game.score1,
|
||||
team2Score: game.score2,
|
||||
status: "completed",
|
||||
playedAt: new Date(),
|
||||
createdById: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Update player stats
|
||||
await updatePlayerStats(player1.id, team1Won, player1EloChange);
|
||||
await updatePlayerStats(player2.id, team1Won, player2EloChange);
|
||||
await updatePlayerStats(player3.id, team2Won, player3EloChange);
|
||||
await updatePlayerStats(player4.id, team2Won, player4EloChange);
|
||||
|
||||
// Update partnership stats
|
||||
await updatePartnershipStats(player1.id, player2.id, team1Won, player1EloChange);
|
||||
await updatePartnershipStats(player3.id, player4.id, team2Won, player3EloChange);
|
||||
|
||||
// Add players as participants if not already added
|
||||
for (const player of [player1, player2, player3, player4]) {
|
||||
try {
|
||||
// Check if participant already exists
|
||||
const existing = await prisma.eventParticipant.findFirst({
|
||||
where: {
|
||||
eventId: tournamentId,
|
||||
playerId: player.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.eventParticipant.create({
|
||||
data: {
|
||||
eventId: tournamentId,
|
||||
playerId: player.id,
|
||||
status: "participated",
|
||||
registrationDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Participant already exists, which is fine
|
||||
console.log(`Participant ${player.id} already exists in tournament ${tournamentId}`);
|
||||
}
|
||||
}
|
||||
|
||||
importedCount++;
|
||||
} catch (err: any) {
|
||||
errors.push(`Game ${index + 1}: ${err.message}`);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
importedCount,
|
||||
errorCount,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Bulk game submission error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Failed to submit games" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function findPlayerByName(name: string) {
|
||||
// Try to find existing player with exact name match first (case-sensitive)
|
||||
let player = await prisma.player.findFirst({
|
||||
where: {
|
||||
name: name,
|
||||
},
|
||||
});
|
||||
|
||||
if (!player) {
|
||||
// Try partial match (case-sensitive)
|
||||
player = await prisma.player.findFirst({
|
||||
where: {
|
||||
name: {
|
||||
contains: name,
|
||||
},
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
async function updatePlayerStats(playerId: number, won: boolean, eloChange: number) {
|
||||
const player = await prisma.player.findUnique({
|
||||
where: { id: playerId },
|
||||
});
|
||||
|
||||
if (!player) {
|
||||
throw new Error(`Player ${playerId} not found`);
|
||||
}
|
||||
|
||||
await prisma.player.update({
|
||||
where: { id: playerId },
|
||||
data: {
|
||||
currentElo: player.currentElo + Math.round(eloChange),
|
||||
gamesPlayed: player.gamesPlayed + 1,
|
||||
wins: won ? player.wins + 1 : player.wins,
|
||||
losses: won ? player.losses : player.losses + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePartnershipStats(player1Id: number, player2Id: number, won: boolean, eloChange: number) {
|
||||
const [smallId, largeId] = player1Id < player2Id ? [player1Id, player2Id] : [player2Id, player1Id];
|
||||
|
||||
const existing = await prisma.partnershipStat.findFirst({
|
||||
where: {
|
||||
player1Id: smallId,
|
||||
player2Id: largeId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await prisma.partnershipStat.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
gamesPlayed: { increment: 1 },
|
||||
wins: won ? { increment: 1 } : undefined,
|
||||
losses: won ? undefined : { increment: 1 },
|
||||
totalEloChange: { increment: eloChange },
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.partnershipStat.create({
|
||||
data: {
|
||||
player1Id: smallId,
|
||||
player2Id: largeId,
|
||||
gamesPlayed: 1,
|
||||
wins: won ? 1 : 0,
|
||||
losses: won ? 0 : 1,
|
||||
totalEloChange: eloChange,
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { canManageTournament } from "@/lib/permissions";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const tournamentId = parseInt(params.id);
|
||||
const body = await request.json();
|
||||
const { playerIds } = body;
|
||||
|
||||
if (!playerIds || !Array.isArray(playerIds)) {
|
||||
return NextResponse.json(
|
||||
{ error: "playerIds array is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
const permission = await canManageTournament(tournamentId);
|
||||
if (!permission.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: permission.reason || 'Insufficient permissions' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Add participants to tournament
|
||||
const participants = await Promise.all(
|
||||
playerIds.map(async (playerId: number) => {
|
||||
try {
|
||||
// Check if participant already exists
|
||||
const existing = await prisma.eventParticipant.findFirst({
|
||||
where: {
|
||||
eventId: tournamentId,
|
||||
playerId: playerId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Create new participant
|
||||
return await prisma.eventParticipant.create({
|
||||
data: {
|
||||
eventId: tournamentId,
|
||||
playerId: playerId,
|
||||
status: "registered",
|
||||
registrationDate: new Date(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to add participant ${playerId}:`, error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const successfulParticipants = participants.filter(p => p !== null);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
added: successfulParticipants.length,
|
||||
participants: successfulParticipants,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to add participants:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to add participants" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user