feat: add site_admin role, casual match support, and tournament deletion
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { canDeleteTournament } from "@/lib/permissions";
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/tournaments/[id]
|
||||
*
|
||||
* Delete a tournament and optionally its associated matches
|
||||
* Options:
|
||||
* - deleteMatches: Delete all matches associated with the tournament
|
||||
* - orphanMatches: Keep matches but remove tournament association (eventId becomes null)
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const tournamentId = parseInt(params.id);
|
||||
const body = await request.json();
|
||||
const { deleteMatches = false } = body;
|
||||
|
||||
// Validate tournament ID
|
||||
if (isNaN(tournamentId)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid tournament ID" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has permission to delete this tournament
|
||||
const permission = await canDeleteTournament(tournamentId);
|
||||
if (!permission.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: permission.reason || "Not authorized to delete this tournament" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify tournament exists
|
||||
const tournament = await prisma.event.findUnique({
|
||||
where: { id: tournamentId },
|
||||
include: { matches: true },
|
||||
});
|
||||
|
||||
if (!tournament) {
|
||||
return NextResponse.json(
|
||||
{ error: "Tournament not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const matchCount = tournament.matches.length;
|
||||
|
||||
// Handle matches based on user choice
|
||||
if (deleteMatches) {
|
||||
// Delete all matches associated with the tournament
|
||||
await prisma.match.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
} else {
|
||||
// Orphan matches - remove tournament association
|
||||
await prisma.match.updateMany({
|
||||
where: { eventId: tournamentId },
|
||||
data: { eventId: null },
|
||||
});
|
||||
}
|
||||
|
||||
// Delete event participants
|
||||
await prisma.eventParticipant.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete teams
|
||||
await prisma.team.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete tournament rounds
|
||||
await prisma.tournamentRound.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete bracket matchups
|
||||
await prisma.bracketMatchup.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete the tournament itself
|
||||
await prisma.event.delete({
|
||||
where: { id: tournamentId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Tournament deleted successfully",
|
||||
deletedTournament: tournament.name,
|
||||
deletedMatches: deleteMatches ? matchCount : 0,
|
||||
orphanedMatches: deleteMatches ? 0 : matchCount,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error("Error deleting tournament:", error);
|
||||
const message = error instanceof Error ? error.message : "Failed to delete tournament";
|
||||
return NextResponse.json(
|
||||
{ error: message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getSession } from "@/lib/auth-simple";
|
||||
|
||||
import { calculateTeamElo, calculateTeamEloChange } from "@/lib/elo-utils";
|
||||
|
||||
const K_FACTOR = 32; // Standard K-factor for Elo calculations
|
||||
|
||||
/**
|
||||
* POST /api/matches
|
||||
*
|
||||
* Create a casual match (not associated with a tournament)
|
||||
* Requires: player role or higher
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
team1P1Id,
|
||||
team1P2Id,
|
||||
team2P1Id,
|
||||
team2P2Id,
|
||||
team1Score,
|
||||
team2Score,
|
||||
playedAt,
|
||||
isCasual = true,
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!team1P1Id || !team1P2Id || !team2P1Id || !team2P2Id) {
|
||||
return NextResponse.json(
|
||||
{ error: "All four player IDs are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (team1Score === undefined || team2Score === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: "Both team scores are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check permissions - need at least player role
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ error: "Not authenticated" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = session.user?.id || null;
|
||||
|
||||
// Fetch player data
|
||||
const [player1, player2, player3, player4] = await Promise.all([
|
||||
prisma.player.findUnique({ where: { id: parseInt(team1P1Id) } }),
|
||||
prisma.player.findUnique({ where: { id: parseInt(team1P2Id) } }),
|
||||
prisma.player.findUnique({ where: { id: parseInt(team2P1Id) } }),
|
||||
prisma.player.findUnique({ where: { id: parseInt(team2P2Id) } }),
|
||||
]);
|
||||
|
||||
// Check all players exist
|
||||
const missingPlayers: number[] = [];
|
||||
if (!player1) missingPlayers.push(parseInt(team1P1Id));
|
||||
if (!player2) missingPlayers.push(parseInt(team1P2Id));
|
||||
if (!player3) missingPlayers.push(parseInt(team2P1Id));
|
||||
if (!player4) missingPlayers.push(parseInt(team2P2Id));
|
||||
|
||||
if (missingPlayers.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: `Players not found: ${missingPlayers.join(", ")}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate Elo changes
|
||||
const team1Rating = calculateTeamElo(player1!.currentElo, player2!.currentElo);
|
||||
const team2Rating = calculateTeamElo(player3!.currentElo, player4!.currentElo);
|
||||
|
||||
const team1Won = team1Score > team2Score;
|
||||
const team2Won = team2Score > team1Score;
|
||||
const isTie = team1Score === team2Score;
|
||||
|
||||
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 (eventId is null for casual matches)
|
||||
const match = await prisma.match.create({
|
||||
data: {
|
||||
eventId: null, // No tournament association for casual matches
|
||||
isCasual: isCasual,
|
||||
playedAt: playedAt ? new Date(playedAt) : new Date(),
|
||||
team1P1Id: parseInt(team1P1Id),
|
||||
team1P2Id: parseInt(team1P2Id),
|
||||
team2P1Id: parseInt(team2P1Id),
|
||||
team2P2Id: parseInt(team2P2Id),
|
||||
team1Score,
|
||||
team2Score,
|
||||
status: "completed",
|
||||
createdById: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Update player stats (only if it's a casual match - tournament matches update via their own flow)
|
||||
if (isCasual) {
|
||||
await updatePlayerStats(parseInt(team1P1Id), team1Won, player1EloChange);
|
||||
await updatePlayerStats(parseInt(team1P2Id), team1Won, player2EloChange);
|
||||
await updatePlayerStats(parseInt(team2P1Id), team2Won, player3EloChange);
|
||||
await updatePlayerStats(parseInt(team2P2Id), team2Won, player4EloChange);
|
||||
|
||||
// Update partnership stats for casual matches
|
||||
await updatePartnershipStats(parseInt(team1P1Id), parseInt(team1P2Id), team1Won, player1EloChange);
|
||||
await updatePartnershipStats(parseInt(team2P1Id), parseInt(team2P2Id), team2Won, player3EloChange);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
match,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error("Error creating casual match:", error);
|
||||
const message = error instanceof Error ? error.message : "Failed to create match";
|
||||
return NextResponse.json(
|
||||
{ error: message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user