feat(api): add tournaments endpoints
Create comprehensive tournament management API with: - GET/POST /api/tournaments for listing and creating tournaments - GET/PUT/DELETE /api/tournaments/[id] for tournament CRUD operations - GET/POST /api/tournaments/[id]/inscriptions for team registration - PUT/DELETE /api/tournaments/[id]/inscriptions/[inscriptionId] for inscription management - POST /api/tournaments/[id]/generate-bracket for single elimination bracket generation - GET/PUT /api/tournaments/[id]/matches/[matchId] for match results and automatic winner advancement Features bracket generation with proper seeding, bye handling for non-power-of-2 team counts, and automatic winner advancement to next round matches. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { z } from 'zod';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string; inscriptionId: string }>;
|
||||
}
|
||||
|
||||
// Validation schema for updating an inscription
|
||||
const updateInscriptionSchema = z.object({
|
||||
player1Id: z.string().cuid('Invalid player 1 ID').optional(),
|
||||
player2Id: z.string().cuid('Invalid player 2 ID').optional().nullable(),
|
||||
teamName: z.string().max(100).optional().nullable(),
|
||||
isPaid: z.boolean().optional(),
|
||||
paidAmount: z.number().nonnegative().optional(),
|
||||
seedNumber: z.number().int().positive().optional(),
|
||||
notes: z.string().max(500).optional().nullable(),
|
||||
});
|
||||
|
||||
// PUT /api/tournaments/[id]/inscriptions/[inscriptionId] - Update inscription
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
context: RouteContext
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id, inscriptionId } = await context.params;
|
||||
|
||||
// Verify tournament exists and belongs to user's organization
|
||||
const tournament = await db.tournament.findFirst({
|
||||
where: {
|
||||
id,
|
||||
organizationId: session.user.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!tournament) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tournament not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify inscription exists
|
||||
const existingInscription = await db.tournamentInscription.findFirst({
|
||||
where: {
|
||||
id: inscriptionId,
|
||||
tournamentId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingInscription) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Inscription not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const validationResult = updateInscriptionSchema.safeParse(body);
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid inscription data',
|
||||
details: validationResult.error.flatten().fieldErrors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { player1Id, player2Id, teamName, isPaid, paidAmount, seedNumber, notes } = validationResult.data;
|
||||
|
||||
// Build update data
|
||||
interface InscriptionUpdateData {
|
||||
clientId?: string;
|
||||
partnerId?: string | null;
|
||||
teamName?: string | null;
|
||||
isPaid?: boolean;
|
||||
paidAmount?: Decimal;
|
||||
seedNumber?: number;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
const updateData: InscriptionUpdateData = {};
|
||||
|
||||
// Validate player 1 if changing
|
||||
if (player1Id !== undefined && player1Id !== existingInscription.clientId) {
|
||||
const player1 = await db.client.findFirst({
|
||||
where: {
|
||||
id: player1Id,
|
||||
organizationId: session.user.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!player1) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Player 1 not found or does not belong to your organization' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check player 1 is not already registered in another inscription
|
||||
const existingPlayer1 = await db.tournamentInscription.findFirst({
|
||||
where: {
|
||||
tournamentId: id,
|
||||
id: { not: inscriptionId },
|
||||
OR: [
|
||||
{ clientId: player1Id },
|
||||
{ partnerId: player1Id },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingPlayer1) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Player 1 is already registered in another team' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
updateData.clientId = player1Id;
|
||||
}
|
||||
|
||||
// Validate player 2 if changing
|
||||
if (player2Id !== undefined) {
|
||||
if (player2Id === null) {
|
||||
updateData.partnerId = null;
|
||||
} else if (player2Id !== existingInscription.partnerId) {
|
||||
const player2 = await db.client.findFirst({
|
||||
where: {
|
||||
id: player2Id,
|
||||
organizationId: session.user.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!player2) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Player 2 not found or does not belong to your organization' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check player 2 is not already registered in another inscription
|
||||
const existingPlayer2 = await db.tournamentInscription.findFirst({
|
||||
where: {
|
||||
tournamentId: id,
|
||||
id: { not: inscriptionId },
|
||||
OR: [
|
||||
{ clientId: player2Id },
|
||||
{ partnerId: player2Id },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingPlayer2) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Player 2 is already registered in another team' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
updateData.partnerId = player2Id;
|
||||
}
|
||||
}
|
||||
|
||||
if (teamName !== undefined) updateData.teamName = teamName;
|
||||
if (isPaid !== undefined) updateData.isPaid = isPaid;
|
||||
if (paidAmount !== undefined) updateData.paidAmount = new Decimal(paidAmount);
|
||||
if (seedNumber !== undefined) updateData.seedNumber = seedNumber;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const inscription = await db.tournamentInscription.update({
|
||||
where: { id: inscriptionId },
|
||||
data: updateData,
|
||||
include: {
|
||||
client: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get partner info if exists
|
||||
let partner = null;
|
||||
if (inscription.partnerId) {
|
||||
partner = await db.client.findUnique({
|
||||
where: { id: inscription.partnerId },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
level: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...inscription,
|
||||
partner,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating inscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update inscription' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/tournaments/[id]/inscriptions/[inscriptionId] - Remove inscription
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
context: RouteContext
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id, inscriptionId } = await context.params;
|
||||
|
||||
// Verify tournament exists and belongs to user's organization
|
||||
const tournament = await db.tournament.findFirst({
|
||||
where: {
|
||||
id,
|
||||
organizationId: session.user.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!tournament) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tournament not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow removing inscriptions if tournament is still in registration phase
|
||||
if (!['DRAFT', 'REGISTRATION_OPEN'].includes(tournament.status)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot remove inscriptions after tournament has started' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify inscription exists
|
||||
const existingInscription = await db.tournamentInscription.findFirst({
|
||||
where: {
|
||||
id: inscriptionId,
|
||||
tournamentId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingInscription) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Inscription not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Delete inscription
|
||||
await db.tournamentInscription.delete({
|
||||
where: { id: inscriptionId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Inscription removed successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing inscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to remove inscription' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user