feat: Implement tournament schedule tab and fix E2E tests (#27)
## 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>
This commit was merged in pull request #27.
This commit is contained in:
@@ -3,18 +3,12 @@ import { prisma } from "@/lib/prisma";
|
||||
import { canManageTournament, canDeleteTournament } from "@/lib/permissions";
|
||||
import { getTournamentStatus } from "@/lib/tournamentUtils";
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/tournaments/[id]
|
||||
*
|
||||
* Get a single tournament by ID
|
||||
*/
|
||||
export async function GET(request: Request, { params }: RouteParams) {
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const tournamentId = parseInt(id);
|
||||
@@ -45,28 +39,14 @@ export async function GET(request: Request, { params }: RouteParams) {
|
||||
player: true,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
include: {
|
||||
player1: true,
|
||||
player2: true,
|
||||
},
|
||||
},
|
||||
rounds: {
|
||||
include: {
|
||||
bracketMatchups: {
|
||||
include: {
|
||||
team1: {
|
||||
include: {
|
||||
player1: true,
|
||||
player2: true,
|
||||
},
|
||||
},
|
||||
team2: {
|
||||
include: {
|
||||
player1: true,
|
||||
player2: true,
|
||||
},
|
||||
},
|
||||
player1P1: true,
|
||||
player1P2: true,
|
||||
player2P1: true,
|
||||
player2P2: true,
|
||||
match: true,
|
||||
},
|
||||
},
|
||||
@@ -104,7 +84,7 @@ export async function GET(request: Request, { params }: RouteParams) {
|
||||
*
|
||||
* Update a tournament by ID
|
||||
*/
|
||||
export async function PUT(request: Request, { params }: RouteParams) {
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const tournamentId = parseInt(id);
|
||||
@@ -144,33 +124,43 @@ export async function PUT(request: Request, { params }: RouteParams) {
|
||||
description,
|
||||
eventDate,
|
||||
eventType,
|
||||
tournamentType,
|
||||
format,
|
||||
status,
|
||||
maxParticipants,
|
||||
ownerId,
|
||||
targetScore,
|
||||
allowTies,
|
||||
teamDurability,
|
||||
partnerRotation,
|
||||
allowByes,
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Tournament name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
// 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
|
||||
const updateData: Record<string, unknown> = {
|
||||
name: name.trim(),
|
||||
description: description || null,
|
||||
eventDate: eventDate ? new Date(eventDate) : null,
|
||||
eventType: eventType || "tournament",
|
||||
format: format || "round_robin",
|
||||
maxParticipants: maxParticipants ? parseInt(maxParticipants) : null,
|
||||
targetScore: targetScore ? parseInt(targetScore) : null,
|
||||
allowTies: allowTies ?? false,
|
||||
};
|
||||
// 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) {
|
||||
@@ -219,7 +209,7 @@ export async function PUT(request: Request, { params }: RouteParams) {
|
||||
* - 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 }: RouteParams) {
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const tournamentId = parseInt(id);
|
||||
@@ -285,11 +275,6 @@ export async function DELETE(request: Request, { params }: RouteParams) {
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete teams
|
||||
await prisma.team.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
});
|
||||
|
||||
// Delete tournament rounds
|
||||
await prisma.tournamentRound.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
|
||||
Reference in New Issue
Block a user