diff --git a/src/app/admin/tournaments/[id]/schedule/page.tsx b/src/app/admin/tournaments/[id]/schedule/page.tsx new file mode 100644 index 0000000..a6fea79 --- /dev/null +++ b/src/app/admin/tournaments/[id]/schedule/page.tsx @@ -0,0 +1,219 @@ +import { prisma } from "@/lib/prisma" +export const dynamic = "force-dynamic"; +import Navigation from "@/components/Navigation" +import Link from "next/link" +import { notFound, redirect } from "next/navigation" +import { canManageTournament } from "@/lib/permissions" +import { ScheduleGenerator } from "@/components/ScheduleGenerator" + +interface PageProps { + params: { + id: string + } +} + +const statusColors: Record = { + pending: "bg-gray-100 text-gray-700", + in_progress: "bg-yellow-100 text-yellow-800", + completed: "bg-green-100 text-green-800", +} + +export default async function TournamentSchedulePage({ params }: PageProps) { + const { id } = await params + const tournamentId = parseInt(id, 10) + + if (isNaN(tournamentId)) { + notFound() + } + + const permission = await canManageTournament(tournamentId) + if (!permission.allowed) { + redirect("/auth/login") + } + + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { + teams: { + include: { + player1: true, + player2: true, + }, + }, + rounds: { + orderBy: { roundNumber: "asc" }, + include: { + bracketMatchups: { + include: { + team1: { + include: { player1: true, player2: true }, + }, + team2: { + include: { player1: true, player2: true }, + }, + match: true, + }, + }, + }, + }, + }, + }) + + if (!tournament) { + notFound() + } + + const hasSchedule = tournament.rounds.length > 0 + + return ( +
+ + +
+
+ {/* Breadcrumb */} + + + {/* Page Header */} +
+

Tournament Schedule

+

+ {tournament.name} — {tournament.teams.length} teams +

+
+ + {/* Schedule Generator (when no schedule exists) */} + {!hasSchedule && ( +
+

+ No Schedule Generated +

+ +
+ )} + + {/* Schedule Rounds */} + {hasSchedule && tournament.rounds.map((round) => ( +
+
+

+ Round {round.roundNumber} +

+ + {round.status.replace("_", " ")} + +
+ + {round.bracketMatchups.length === 0 ? ( +

No matchups in this round.

+ ) : ( +
+ {round.bracketMatchups.map((matchup) => { + const team1Name = matchup.team1 + ? `${matchup.team1.player1.name} + ${matchup.team1.player2.name}` + : "TBD" + const team2Name = matchup.team2 + ? `${matchup.team2.player1.name} + ${matchup.team2.player2.name}` + : "TBD" + + return ( +
+
+
+
+ {matchup.tableNumber && ( + + Table {matchup.tableNumber} + + )} + + {matchup.status.replace("_", " ")} + +
+

+ {team1Name} vs {team2Name} +

+
+ +
+ {matchup.match ? ( +
+ matchup.match.team2Score + ? 'text-green-600' + : 'text-gray-900' + }`}> + {matchup.match.team1Score} + + - + matchup.match.team1Score + ? 'text-green-600' + : 'text-gray-900' + }`}> + {matchup.match.team2Score} + + + View + +
+ ) : ( + + Enter Result + + )} +
+
+
+ ) + })} +
+ )} +
+ ))} + + {/* Actions when schedule exists */} + {hasSchedule && ( +
+

+ Schedule Actions +

+ +
+ )} +
+
+
+ ) +} diff --git a/src/components/ScheduleGenerator.tsx b/src/components/ScheduleGenerator.tsx new file mode 100644 index 0000000..f49ff39 --- /dev/null +++ b/src/components/ScheduleGenerator.tsx @@ -0,0 +1,126 @@ +"use client" + +import { useState } from "react" + +interface ScheduleGeneratorProps { + tournamentId: number + teamCount: number +} + +export function ScheduleGenerator({ tournamentId, teamCount }: ScheduleGeneratorProps) { + const [isGenerating, setIsGenerating] = useState(false) + const [error, setError] = useState("") + const [result, setResult] = useState<{ + roundsCreated: number + matchupsCreated: number + } | null>(null) + + const handleGenerate = async () => { + setError("") + setResult(null) + setIsGenerating(true) + + try { + const response = await fetch(`/api/tournaments/${tournamentId}/schedule`, { + method: "POST", + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.error || "Failed to generate schedule") + setIsGenerating(false) + return + } + + setResult({ + roundsCreated: data.roundsCreated, + matchupsCreated: data.matchupsCreated, + }) + setIsGenerating(false) + + // Reload to show the schedule + setTimeout(() => { + window.location.reload() + }, 1500) + } catch { + setError("An error occurred. Please try again.") + setIsGenerating(false) + } + } + + const handleDelete = async () => { + setError("") + setIsGenerating(true) + + try { + const response = await fetch(`/api/tournaments/${tournamentId}/schedule`, { + method: "DELETE", + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.error || "Failed to delete schedule") + setIsGenerating(false) + return + } + + window.location.reload() + } catch { + setError("An error occurred. Please try again.") + setIsGenerating(false) + } + } + + const expectedRounds = teamCount % 2 === 0 ? teamCount - 1 : teamCount + const expectedMatchups = (teamCount * (teamCount - 1)) / 2 + + return ( +
+ {error && ( +
+
{error}
+
+ )} + + {result && ( +
+
+ Generated {result.roundsCreated} rounds with {result.matchupsCreated} matchups! +
+
+ )} + +
+

+ Generate a round-robin schedule for {teamCount} teams. +

+

+ This will create {expectedRounds} rounds with {expectedMatchups} total matchups. + Each team plays every other team exactly once. +

+
+ +
+ + + +
+
+ ) +}