feat: add player profiles, schedule, tournaments admin, and CSV upload API
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
eventId,
|
||||
team1P1Id,
|
||||
team1P2Id,
|
||||
team2P1Id,
|
||||
team2P2Id,
|
||||
team1Score,
|
||||
team2Score,
|
||||
playedAt,
|
||||
} = body
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!team1P1Id ||
|
||||
!team1P2Id ||
|
||||
!team2P1Id ||
|
||||
!team2P2Id ||
|
||||
team1Score === undefined ||
|
||||
team2Score === undefined
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create the match
|
||||
const match = await prisma.match.create({
|
||||
data: {
|
||||
eventId: eventId || null,
|
||||
team1P1Id,
|
||||
team1P2Id,
|
||||
team2P1Id,
|
||||
team2P2Id,
|
||||
team1Score,
|
||||
team2Score,
|
||||
playedAt: playedAt ? new Date(playedAt) : new Date(),
|
||||
status: "completed",
|
||||
},
|
||||
})
|
||||
|
||||
// Calculate Elo changes for all players
|
||||
const players = [
|
||||
{ id: team1P1Id },
|
||||
{ id: team1P2Id },
|
||||
{ id: team2P1Id },
|
||||
{ id: team2P2Id },
|
||||
]
|
||||
|
||||
const team1Won = team1Score > team2Score
|
||||
const K = 32 // K-factor for Elo calculation
|
||||
|
||||
for (const player of players) {
|
||||
const playerData = await prisma.player.findUnique({
|
||||
where: { id: player.id },
|
||||
})
|
||||
|
||||
if (!playerData) continue
|
||||
|
||||
const isTeam1 = player.id === team1P1Id || player.id === team1P2Id
|
||||
const expectedScore = 1 / (1 + Math.pow(10, (1200 - playerData.currentElo) / 400))
|
||||
const actualScore = isTeam1 ? (team1Won ? 1 : 0) : (team1Won ? 0 : 1)
|
||||
|
||||
const ratingChange = Math.round(K * (actualScore - expectedScore))
|
||||
const newRating = playerData.currentElo + ratingChange
|
||||
|
||||
// Update player's current Elo
|
||||
await prisma.player.update({
|
||||
where: { id: player.id },
|
||||
data: { currentElo: newRating },
|
||||
})
|
||||
|
||||
// Create Elo snapshot
|
||||
await prisma.eloSnapshot.create({
|
||||
data: {
|
||||
playerId: player.id,
|
||||
matchId: match.id,
|
||||
ratingBefore: playerData.currentElo,
|
||||
ratingAfter: newRating,
|
||||
ratingChange,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Track partnership games
|
||||
await prisma.partnershipGame.create({
|
||||
data: {
|
||||
player1Id: team1P1Id,
|
||||
player2Id: team1P2Id,
|
||||
matchId: match.id,
|
||||
teamNumber: 1,
|
||||
wonMatch: team1Won ? 1 : 0,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.partnershipGame.create({
|
||||
data: {
|
||||
player1Id: team2P1Id,
|
||||
player2Id: team2P2Id,
|
||||
matchId: match.id,
|
||||
teamNumber: 2,
|
||||
wonMatch: team1Won ? 0 : 1,
|
||||
},
|
||||
})
|
||||
|
||||
// Update partnership statistics
|
||||
const partnerships = [
|
||||
[team1P1Id, team1P2Id],
|
||||
[team2P1Id, team2P2Id],
|
||||
]
|
||||
|
||||
for (const [p1Id, p2Id] of partnerships) {
|
||||
// Ensure p1Id < p2Id for consistent lookup
|
||||
const [player1Id, player2Id] = p1Id < p2Id ? [p1Id, p2Id] : [p2Id, p1Id]
|
||||
|
||||
const existingStat = await prisma.partnershipStat.findFirst({
|
||||
where: {
|
||||
player1Id,
|
||||
player2Id,
|
||||
},
|
||||
})
|
||||
|
||||
if (existingStat) {
|
||||
const won = (player1Id === team1P1Id && player2Id === team1P2Id && team1Won) ||
|
||||
(player1Id === team2P1Id && player2Id === team2P2Id && !team1Won)
|
||||
|
||||
await prisma.partnershipStat.update({
|
||||
where: { id: existingStat.id },
|
||||
data: {
|
||||
gamesPlayed: { increment: 1 },
|
||||
wins: { increment: won ? 1 : 0 },
|
||||
losses: { increment: won ? 0 : 1 },
|
||||
totalEloChange: { increment: 0 }, // Would need to calculate per player
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const won = (player1Id === team1P1Id && player2Id === team1P2Id && team1Won) ||
|
||||
(player1Id === team2P1Id && player2Id === team2P2Id && !team1Won)
|
||||
|
||||
await prisma.partnershipStat.create({
|
||||
data: {
|
||||
player1Id,
|
||||
player2Id,
|
||||
gamesPlayed: 1,
|
||||
wins: won ? 1 : 0,
|
||||
losses: won ? 0 : 1,
|
||||
totalEloChange: 0,
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, match })
|
||||
} catch (error) {
|
||||
console.error("Error creating match:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create match" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const playerId = searchParams.get("playerId")
|
||||
|
||||
let matches
|
||||
|
||||
if (playerId) {
|
||||
const id = parseInt(playerId)
|
||||
matches = await prisma.match.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ team1P1Id: id },
|
||||
{ team1P2Id: id },
|
||||
{ team2P1Id: id },
|
||||
{ team2P2Id: id },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
event: true,
|
||||
team1P1: true,
|
||||
team1P2: true,
|
||||
team2P1: true,
|
||||
team2P2: true,
|
||||
},
|
||||
orderBy: { playedAt: "desc" },
|
||||
take: 50,
|
||||
})
|
||||
} else {
|
||||
matches = await prisma.match.findMany({
|
||||
include: {
|
||||
event: true,
|
||||
team1P1: true,
|
||||
team1P2: true,
|
||||
team2P1: true,
|
||||
team2P2: true,
|
||||
},
|
||||
orderBy: { playedAt: "desc" },
|
||||
take: 50,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ matches })
|
||||
} catch (error) {
|
||||
console.error("Error fetching matches:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch matches" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Papa = require("papaparse")
|
||||
|
||||
// Map table names to numeric IDs
|
||||
const TABLE_NAME_MAP: Record<string, number> = {
|
||||
"Clubs": 1,
|
||||
"Hearts": 2,
|
||||
"Diamonds": 3,
|
||||
"Spades": 4,
|
||||
"Stars": 5,
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get("csvFile") as File
|
||||
const eventId = formData.get("eventId") as string
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 })
|
||||
}
|
||||
|
||||
const fileBuffer = Buffer.from(await file.arrayBuffer())
|
||||
const fileText = fileBuffer.toString()
|
||||
|
||||
const parseResult = Papa.parse(fileText, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
})
|
||||
|
||||
if (parseResult.errors.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "CSV parsing error", details: parseResult.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const results = parseResult.data as Record<string, string>[]
|
||||
|
||||
let importedCount = 0
|
||||
let errorCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
for (const row of results) {
|
||||
try {
|
||||
// Extract and validate data
|
||||
const roundNumber = parseInt(row.Round)
|
||||
const tableName = row.Table
|
||||
const seat1Name = row["Seat 1"]?.trim()
|
||||
const seat3Name = row["Seat 3"]?.trim()
|
||||
const oddsPoints = parseInt(row["Odds Points"])
|
||||
const seat2Name = row["Seat 2"]?.trim()
|
||||
const seat4Name = row["Seat 4"]?.trim()
|
||||
const evensPoints = parseInt(row["Evens Points"])
|
||||
|
||||
// Validate required fields
|
||||
if (!seat1Name || !seat3Name || !seat2Name || !seat4Name) {
|
||||
errors.push(`Row ${importedCount + errorCount + 2}: Missing player names`)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Find or create players
|
||||
const players = await Promise.all(
|
||||
[seat1Name, seat3Name, seat2Name, seat4Name].map(async (name) => {
|
||||
let player = await prisma.player.findUnique({ where: { name } })
|
||||
if (!player) {
|
||||
player = await prisma.player.create({ data: { name } })
|
||||
}
|
||||
return player
|
||||
})
|
||||
)
|
||||
|
||||
const [player1, player2, player3, player4] = players
|
||||
|
||||
// Map table name to number
|
||||
const tableNumber = TABLE_NAME_MAP[tableName] || 0
|
||||
|
||||
// Find or create round
|
||||
let round = await prisma.tournamentRound.findFirst({
|
||||
where: { eventId: parseInt(eventId), roundNumber },
|
||||
})
|
||||
|
||||
if (!round) {
|
||||
round = await prisma.tournamentRound.create({
|
||||
data: {
|
||||
eventId: parseInt(eventId),
|
||||
roundNumber,
|
||||
status: "completed",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Determine winner
|
||||
const team1Won = oddsPoints > evensPoints
|
||||
|
||||
// Create match
|
||||
const match = await prisma.match.create({
|
||||
data: {
|
||||
eventId: parseInt(eventId),
|
||||
team1P1Id: player1.id,
|
||||
team1P2Id: player2.id,
|
||||
team2P1Id: player3.id,
|
||||
team2P2Id: player4.id,
|
||||
team1Score: oddsPoints,
|
||||
team2Score: evensPoints,
|
||||
playedAt: new Date(),
|
||||
status: "completed",
|
||||
},
|
||||
})
|
||||
|
||||
// Calculate Elo for all players
|
||||
const playersForElo = [
|
||||
{ id: player1.id, isTeam1: true },
|
||||
{ id: player2.id, isTeam1: true },
|
||||
{ id: player3.id, isTeam1: false },
|
||||
{ id: player4.id, isTeam1: false },
|
||||
]
|
||||
|
||||
const K = 32
|
||||
|
||||
for (const p of playersForElo) {
|
||||
const playerData = await prisma.player.findUnique({
|
||||
where: { id: p.id },
|
||||
})
|
||||
|
||||
if (!playerData) continue
|
||||
|
||||
const expectedScore = 1 / (1 + Math.pow(10, (1200 - playerData.currentElo) / 400))
|
||||
const actualScore = p.isTeam1 ? (team1Won ? 1 : 0) : (team1Won ? 0 : 1)
|
||||
|
||||
const ratingChange = Math.round(K * (actualScore - expectedScore))
|
||||
const newRating = playerData.currentElo + ratingChange
|
||||
|
||||
await prisma.player.update({
|
||||
where: { id: p.id },
|
||||
data: { currentElo: newRating },
|
||||
})
|
||||
|
||||
await prisma.eloSnapshot.create({
|
||||
data: {
|
||||
playerId: p.id,
|
||||
matchId: match.id,
|
||||
ratingBefore: playerData.currentElo,
|
||||
ratingAfter: newRating,
|
||||
ratingChange,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Track partnership games
|
||||
await prisma.partnershipGame.create({
|
||||
data: {
|
||||
player1Id: player1.id,
|
||||
player2Id: player2.id,
|
||||
matchId: match.id,
|
||||
teamNumber: 1,
|
||||
wonMatch: team1Won ? 1 : 0,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.partnershipGame.create({
|
||||
data: {
|
||||
player1Id: player3.id,
|
||||
player2Id: player4.id,
|
||||
matchId: match.id,
|
||||
teamNumber: 2,
|
||||
wonMatch: team1Won ? 0 : 1,
|
||||
},
|
||||
})
|
||||
|
||||
// Update partnership statistics
|
||||
const partnerships = [
|
||||
[player1.id, player2.id],
|
||||
[player3.id, player4.id],
|
||||
]
|
||||
|
||||
for (const [p1Id, p2Id] of partnerships) {
|
||||
const [player1Id, player2Id] = p1Id < p2Id ? [p1Id, p2Id] : [p2Id, p1Id]
|
||||
|
||||
const existingStat = await prisma.partnershipStat.findFirst({
|
||||
where: { player1Id, player2Id },
|
||||
})
|
||||
|
||||
const won =
|
||||
(player1Id === player1.id && player2Id === player2.id && team1Won) ||
|
||||
(player1Id === player3.id && player2Id === player4.id && !team1Won)
|
||||
|
||||
if (existingStat) {
|
||||
await prisma.partnershipStat.update({
|
||||
where: { id: existingStat.id },
|
||||
data: {
|
||||
gamesPlayed: { increment: 1 },
|
||||
wins: { increment: won ? 1 : 0 },
|
||||
losses: { increment: won ? 0 : 1 },
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await prisma.partnershipStat.create({
|
||||
data: {
|
||||
player1Id,
|
||||
player2Id,
|
||||
gamesPlayed: 1,
|
||||
wins: won ? 1 : 0,
|
||||
losses: won ? 0 : 1,
|
||||
totalEloChange: 0,
|
||||
lastPlayed: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
importedCount++
|
||||
} catch (err: any) {
|
||||
errors.push(`Row ${importedCount + errorCount + 2}: ${err.message}`)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
importedCount,
|
||||
errorCount,
|
||||
errors: errorCount > 0 ? errors : undefined,
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("Error uploading CSV:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to process CSV file", details: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user