Files
euchre_camp/src/app/api/matches/upload/route.ts
T

281 lines
8.8 KiB
TypeScript

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import Papa from "papaparse";
import { canManageTournament } from "@/lib/permissions";
import { getSession } from "@/lib/auth-simple";
import { calculateTeamElo, calculateTeamEloChange } from "@/lib/elo-utils";
interface CSVRow {
"Event #": string;
Round: string;
Table: string;
"Seat 1": string;
"Seat 3": string;
"Odds Points": string;
"Seat 2": string;
"Seat 4": string;
"Evens Points": string;
Winner?: string;
}
const K_FACTOR = 32; // Standard K-factor for Elo calculations
export async function POST(request: Request) {
try {
const formData = await request.formData();
const csvFile = formData.get("csvFile") as File;
const eventId = formData.get("eventId") as string;
if (!csvFile) {
return NextResponse.json(
{ error: "No CSV file provided" },
{ status: 400 }
);
}
if (!eventId) {
return NextResponse.json(
{ error: "No tournament selected" },
{ status: 400 }
);
}
// Check if user has permission to add matches to this tournament
const permission = await canManageTournament(parseInt(eventId));
if (!permission.allowed) {
return NextResponse.json(
{ error: permission.reason || 'Insufficient permissions to add matches' },
{ status: 403 }
);
}
// Get the current user to assign as match creator
const session = await getSession();
const userId = session?.user?.id || null;
// Read and parse CSV
const csvText = await csvFile.text();
const parseResult = Papa.parse<CSVRow>(csvText, {
header: true,
skipEmptyLines: true,
});
if (parseResult.errors.length > 0) {
return NextResponse.json(
{ error: "CSV parsing errors", errors: parseResult.errors.map((e: { message: string }) => e.message) },
{ status: 400 }
);
}
const rows = parseResult.data;
let importedCount = 0;
let errorCount = 0;
const errors: string[] = [];
// Process each row
for (const [index, row] of rows.entries()) {
try {
// Extract player names
const player1Name = row["Seat 1"]?.trim();
const player2Name = row["Seat 3"]?.trim();
const player3Name = row["Seat 2"]?.trim();
const player4Name = row["Seat 4"]?.trim();
if (!player1Name || !player2Name || !player3Name || !player4Name) {
errors.push(`Row ${index + 2}: Missing player names`);
errorCount++;
continue;
}
// Find or create players
const players = await Promise.all([
findOrCreatePlayer(player1Name),
findOrCreatePlayer(player2Name),
findOrCreatePlayer(player3Name),
findOrCreatePlayer(player4Name),
]);
console.log('Players found/created:');
players.forEach((p, i) => {
console.log(` Player ${i + 1}: ${p.name} (ID: ${p.id}, Elo: ${p.currentElo})`);
});
// Parse scores
const team1Score = parseInt(row["Odds Points"]) || 0;
const team2Score = parseInt(row["Evens Points"]) || 0;
// Determine winner
const team1Won = team1Score > team2Score;
const team2Won = team2Score > team1Score;
const isTie = team1Score === team2Score;
// Calculate Elo ratings using standard formula
const team1Rating = calculateTeamElo(players[0].currentElo, players[1].currentElo);
const team2Rating = calculateTeamElo(players[2].currentElo, players[3].currentElo);
// Actual scores (1 for win, 0.5 for tie, 0 for loss)
const team1ScoreActual = isTie ? 0.5 : (team1Won ? 1 : 0);
const team2ScoreActual = isTie ? 0.5 : (team2Won ? 1 : 0);
// Elo change for each team
const team1EloChange = calculateTeamEloChange(team1Rating, team2Rating, team1ScoreActual, K_FACTOR);
const team2EloChange = calculateTeamEloChange(team2Rating, team1Rating, team2ScoreActual, K_FACTOR);
// Individual Elo changes (split evenly between team members)
const player1EloChange = team1EloChange / 2;
const player2EloChange = team1EloChange / 2;
const player3EloChange = team2EloChange / 2;
const player4EloChange = team2EloChange / 2;
// Create match
await prisma.match.create({
data: {
eventId: parseInt(eventId),
playedAt: new Date(),
player1P1Id: players[0].id,
player1P2Id: players[1].id,
player2P1Id: players[2].id,
player2P2Id: players[3].id,
team1Score,
team2Score,
status: "completed",
createdById: userId,
},
});
// Update individual player stats
await updatePlayerStats(players[0].id, team1Won, player1EloChange);
await updatePlayerStats(players[1].id, team1Won, player2EloChange);
await updatePlayerStats(players[2].id, team2Won, player3EloChange);
await updatePlayerStats(players[3].id, team2Won, player4EloChange);
// Update partnership stats (for tracking how players perform together)
await updatePartnershipStats(players[0].id, players[1].id, team1Won, player1EloChange);
await updatePartnershipStats(players[2].id, players[3].id, team2Won, player3EloChange);
// Add players as participants in the tournament (if not already added)
for (const player of players) {
try {
await prisma.eventParticipant.create({
data: {
eventId: parseInt(eventId),
playerId: player.id,
status: "participated",
registrationDate: new Date(),
},
});
} catch {
// Participant already exists, which is fine
console.log(`Participant ${player.id} already exists in tournament ${eventId}`);
}
}
importedCount++;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error";
errors.push(`Row ${index + 2}: ${message}`);
errorCount++;
}
}
return NextResponse.json({
importedCount,
errorCount,
errors: errors.length > 0 ? errors : undefined,
});
} catch (error: unknown) {
console.error("Upload error:", error);
const message = error instanceof Error ? error.message : "Failed to upload CSV";
return NextResponse.json(
{ error: message },
{ status: 500 }
);
}
}
async function findOrCreatePlayer(name: string) {
// Normalize the name for deduplication: trim whitespace and convert to lowercase
const normalizedName = name.trim().toLowerCase();
// Try to find existing player with matching normalized name
let player = await prisma.player.findFirst({
where: { normalizedName },
});
if (!player) {
// Create a new player with the original name and normalized name
player = await prisma.player.create({
data: {
name: name.trim(),
normalizedName,
rating: 1000,
currentElo: 1000,
},
});
}
return player;
}
async function updatePlayerStats(playerId: number, won: boolean, eloChange: number) {
// First, get the current player data to calculate the new values
const player = await prisma.player.findUnique({
where: { id: playerId },
});
if (!player) {
throw new Error(`Player ${playerId} not found`);
}
// Update the player's statistics
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) {
// Sort IDs to ensure consistent partnership lookup
const [smallId, largeId] = player1Id < player2Id ? [player1Id, player2Id] : [player2Id, player1Id];
// Find existing partnership
const existing = await prisma.partnershipStat.findFirst({
where: {
player1Id: smallId,
player2Id: largeId,
},
});
if (existing) {
// Update existing partnership
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 {
// Create new partnership
await prisma.partnershipStat.create({
data: {
player1Id: smallId,
player2Id: largeId,
gamesPlayed: 1,
wins: won ? 1 : 0,
losses: won ? 0 : 1,
totalEloChange: eloChange,
lastPlayed: new Date(),
},
});
}
}