bb6be245b7
## Summary This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures. ### Changes Included 1. **Tournament Schedule Feature** - Added tournament schedule page at `/admin/tournaments/[id]/schedule` - Implemented "Generate Schedule" button functionality - Added schedule generation logic for round-robin tournaments 2. **E2E Test Fixes** - Fixed database connection issues in production builds - Improved test reliability with better error handling and debugging - Updated test infrastructure to use environment variables instead of hardcoded values 3. **CI/CD Updates** - Added E2E test job to PR workflow - Configured tests to run against development database - Moved database password to Gitea secrets 4. **Code Quality** - Removed hardcoded passwords from codebase - Improved Prisma client configuration - Enhanced authentication and navigation components ### Test Results All 16 E2E test scenarios are now passing: - Authentication tests: ✅ - Registration tests: ✅ - Tournament schedule tests: ✅ - Player schedule tests: ✅ - Admin navigation tests: ✅ ### Database Configuration - Tests run against `euchre_camp_dev` database - Production builds use environment variables for database configuration - Database password stored in Gitea secrets as `DB_PASSWORD` ### CI Pipeline The PR workflow now includes: 1. Unit tests 2. E2E tests (using production build) 3. Version bump analysis E2E tests must pass before PR can be merged. Reviewed-on: #27 Co-authored-by: David Gwilliam <dhgwilliam@gmail.com> Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
309 lines
9.2 KiB
TypeScript
309 lines
9.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { canManageTournament, canDeleteTournament } from "@/lib/permissions";
|
|
import { getTournamentStatus } from "@/lib/tournamentUtils";
|
|
|
|
/**
|
|
* GET /api/tournaments/[id]
|
|
*
|
|
* Get a single tournament by ID
|
|
*/
|
|
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
const { id } = await params
|
|
const tournamentId = parseInt(id);
|
|
|
|
// Validate tournament ID
|
|
if (isNaN(tournamentId)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid tournament ID" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Check if user can view/manage this tournament
|
|
const permission = await canManageTournament(tournamentId);
|
|
if (!permission.allowed) {
|
|
return NextResponse.json(
|
|
{ error: permission.reason || "Not authorized to view this tournament" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Fetch the tournament
|
|
const tournament = await prisma.event.findUnique({
|
|
where: { id: tournamentId },
|
|
include: {
|
|
participants: {
|
|
include: {
|
|
player: true,
|
|
},
|
|
},
|
|
rounds: {
|
|
include: {
|
|
bracketMatchups: {
|
|
include: {
|
|
player1P1: true,
|
|
player1P2: true,
|
|
player2P1: true,
|
|
player2P2: true,
|
|
match: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!tournament) {
|
|
return NextResponse.json(
|
|
{ error: "Tournament not found" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Update tournament status if needed
|
|
const calculatedStatus = getTournamentStatus(tournament.eventDate);
|
|
if (tournament.status !== calculatedStatus) {
|
|
tournament.status = calculatedStatus;
|
|
}
|
|
|
|
return NextResponse.json({ tournament });
|
|
} catch (error: unknown) {
|
|
console.error("Error fetching tournament:", error);
|
|
const message = error instanceof Error ? error.message : "Failed to fetch tournament";
|
|
return NextResponse.json(
|
|
{ error: message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/tournaments/[id]
|
|
*
|
|
* Update a tournament by ID
|
|
*/
|
|
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
const { id } = await params
|
|
const tournamentId = parseInt(id);
|
|
|
|
// Validate tournament ID
|
|
if (isNaN(tournamentId)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid tournament ID" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Check if user can manage this tournament
|
|
const permission = await canManageTournament(tournamentId);
|
|
if (!permission.allowed) {
|
|
return NextResponse.json(
|
|
{ error: permission.reason || "Not authorized to update this tournament" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Verify tournament exists
|
|
const existingTournament = await prisma.event.findUnique({
|
|
where: { id: tournamentId },
|
|
});
|
|
|
|
if (!existingTournament) {
|
|
return NextResponse.json(
|
|
{ error: "Tournament not found" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const body = await request.json();
|
|
const {
|
|
name,
|
|
description,
|
|
eventDate,
|
|
eventType,
|
|
tournamentType,
|
|
format,
|
|
status,
|
|
maxParticipants,
|
|
ownerId,
|
|
targetScore,
|
|
allowTies,
|
|
teamDurability,
|
|
partnerRotation,
|
|
allowByes,
|
|
} = body;
|
|
|
|
// Validate name only if it's being updated
|
|
if (body.hasOwnProperty('name')) {
|
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "Tournament name is required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Prepare update data - only include fields that are present in the request
|
|
const updateData: Record<string, unknown> = {};
|
|
|
|
if (body.hasOwnProperty('name')) updateData.name = name.trim();
|
|
if (body.hasOwnProperty('description')) updateData.description = description || null;
|
|
if (body.hasOwnProperty('eventDate')) updateData.eventDate = eventDate ? new Date(eventDate) : null;
|
|
if (body.hasOwnProperty('eventType')) updateData.eventType = eventType || "tournament";
|
|
if (body.hasOwnProperty('tournamentType')) updateData.tournamentType = tournamentType || "individual";
|
|
if (body.hasOwnProperty('format')) updateData.format = format || "round_robin";
|
|
if (body.hasOwnProperty('maxParticipants')) updateData.maxParticipants = maxParticipants ? parseInt(maxParticipants) : null;
|
|
if (body.hasOwnProperty('targetScore')) updateData.targetScore = targetScore ? parseInt(targetScore) : null;
|
|
if (body.hasOwnProperty('allowTies')) updateData.allowTies = allowTies ?? false;
|
|
if (body.hasOwnProperty('teamDurability')) updateData.teamDurability = teamDurability || "permanent";
|
|
if (body.hasOwnProperty('partnerRotation')) updateData.partnerRotation = partnerRotation || "none";
|
|
if (body.hasOwnProperty('allowByes')) updateData.allowByes = allowByes ?? true;
|
|
|
|
// Only allow status updates if they don't conflict with auto-calculation
|
|
if (status) {
|
|
const calculatedStatus = getTournamentStatus(eventDate ? new Date(eventDate) : existingTournament.eventDate);
|
|
if (status !== calculatedStatus) {
|
|
// Allow manual status override but log it
|
|
console.log(`Manual status override: ${existingTournament.status} -> ${status} (calculated: ${calculatedStatus})`);
|
|
}
|
|
updateData.status = status;
|
|
}
|
|
|
|
// Update owner if specified (and user has permission to reassign ownership)
|
|
if (ownerId !== undefined) {
|
|
// Only site_admin or club_admin can reassign ownership
|
|
const canReassign = await canDeleteTournament(tournamentId);
|
|
if (canReassign.allowed) {
|
|
updateData.ownerId = ownerId || null;
|
|
}
|
|
}
|
|
|
|
// Update the tournament
|
|
const updatedTournament = await prisma.event.update({
|
|
where: { id: tournamentId },
|
|
data: updateData,
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
tournament: updatedTournament,
|
|
});
|
|
} catch (error: unknown) {
|
|
console.error("Error updating tournament:", error);
|
|
const message = error instanceof Error ? error.message : "Failed to update tournament";
|
|
return NextResponse.json(
|
|
{ error: message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/tournaments/[id]
|
|
*
|
|
* Delete a tournament by ID
|
|
* 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: Promise<{ id: string }> }) {
|
|
try {
|
|
const { id } = await params
|
|
const tournamentId = parseInt(id);
|
|
|
|
// 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 }
|
|
);
|
|
}
|
|
|
|
// Parse request body to get delete options
|
|
let deleteMatches = false;
|
|
try {
|
|
const body = await request.json();
|
|
deleteMatches = body.deleteMatches || false;
|
|
} catch {
|
|
// If no body is provided, default to orphaning matches
|
|
deleteMatches = false;
|
|
}
|
|
|
|
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 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 }
|
|
);
|
|
}
|
|
}
|