import { Link, useFetcher } from "react-router"; import { auth } from "~/lib/auth.server"; import type { Route } from "./+types/admin.tournaments.$id"; import { logger } from "~/lib/logger"; import { getTournamentById, updateTournamentStatus, } from "~/models/tournament"; import { getTournamentResults, upsertTournamentResult, } from "~/models/tournament-result"; import { findCanonicalParticipantsBySport } from "~/models/participant"; import { isUserAdmin } from "~/models/user"; import { syncTournamentResults, type SyncReport, } from "~/services/sync-tournament-results"; import { getSportsSeasonsByTournament, getPrimaryEventForTournament, setPrimaryEvent, deleteScoringEvent, } from "~/models/scoring-event"; import { isBracketMajor } from "~/lib/event-utils"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { ArrowLeft, CheckCircle2, AlertTriangle } from "lucide-react"; import { BatchResultEntry } from "~/components/BatchResultEntry"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [ { title: `${data?.tournament?.name ?? "Tournament"} - Brackt Admin`, }, ]; } export async function loader({ params }: Route.LoaderArgs) { const tournament = await getTournamentById(params.id); if (!tournament) { throw new Response("Not Found", { status: 404 }); } const [results, canonicalParticipants, linkedSportsSeasons, primaryEvent] = await Promise.all([ getTournamentResults(tournament.id), findCanonicalParticipantsBySport(tournament.sportId), getSportsSeasonsByTournament(tournament.id), getPrimaryEventForTournament(tournament.id), ]); // Bracket majors (tennis, CS2) are scored on their primary window's bracket / // stage UI; golf is entered right here. Surface a link for the bracket case. const simulatorType = linkedSportsSeasons[0]?.sportsSeason?.sport?.simulatorType ?? null; const bracketMajor = isBracketMajor(simulatorType); const isCs2 = simulatorType === "cs2_major_qualifying_points"; return { tournament, results, canonicalParticipants, linkedSportsSeasons, primaryEvent, bracketMajor, isCs2, }; } export async function action(args: Route.ActionArgs) { const { request, params } = args; const session = await auth.api.getSession({ headers: request.headers }); const userId = session?.user.id ?? null; const isAdmin = userId ? await isUserAdmin(userId) : false; if (!isAdmin) { throw new Response("Forbidden", { status: 403 }); } const tournament = await getTournamentById(params.id); if (!tournament) { throw new Response("Not Found", { status: 404 }); } const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "batch-upsert-results") { const resultsRaw = formData.get("results"); if (typeof resultsRaw !== "string" || !resultsRaw) { return { success: false as const, error: "Missing results payload", syncReport: null, }; } let parsed: Array<{ participantId: string; placement: number }>; try { parsed = JSON.parse(resultsRaw); } catch { return { success: false as const, error: "Invalid JSON in results payload", syncReport: null, }; } if (!Array.isArray(parsed)) { return { success: false as const, error: "Results payload must be an array", syncReport: null, }; } try { for (const row of parsed) { if ( !row || typeof row.participantId !== "string" || typeof row.placement !== "number" ) { return { success: false as const, error: "Each result must have participantId and placement", syncReport: null, }; } await upsertTournamentResult({ tournamentId: tournament.id, participantId: row.participantId, placement: row.placement, }); } const syncReport = await syncTournamentResults(tournament.id); // Status reflects reality: completed only when every linked window synced. // If any window failed, leave it in_progress so the failure is visible and // retryable rather than masked by a green "completed" badge. const newStatus = syncReport.windowsFailed === 0 ? "completed" : "in_progress"; if (tournament.status !== newStatus) { await updateTournamentStatus(tournament.id, newStatus); } return { success: true as const, error: null, syncReport, }; } catch (error) { logger.error("batch-upsert-results failed:", error); return { success: false as const, error: error instanceof Error ? error.message : "Failed to save results", syncReport: null, }; } } if (intent === "retry-window-sync") { try { const syncReport = await syncTournamentResults(tournament.id); const newStatus = syncReport.windowsFailed === 0 ? "completed" : "in_progress"; if (tournament.status !== newStatus) { await updateTournamentStatus(tournament.id, newStatus); } return { success: true as const, error: null, syncReport }; } catch (error) { logger.error("retry-window-sync failed:", error); return { success: false as const, error: error instanceof Error ? error.message : "Failed to retry sync", syncReport: null, }; } } if (intent === "set-primary") { const eventId = formData.get("eventId"); if (typeof eventId !== "string" || !eventId) { return { success: false as const, error: "Event ID is required", syncReport: null }; } try { await setPrimaryEvent(eventId); return { success: true as const, error: null, syncReport: null }; } catch (error) { logger.error("set-primary failed:", error); return { success: false as const, error: error instanceof Error ? error.message : "Failed to set primary window", syncReport: null, }; } } if (intent === "unlink-window") { const eventId = formData.get("eventId"); if (typeof eventId !== "string" || !eventId) { return { success: false as const, error: "Event ID is required", syncReport: null }; } try { // Remove that season's scoring event. The tournament stays (we're on its // page); if this was the primary window, another is auto-promoted. await deleteScoringEvent(eventId); return { success: true as const, error: null, syncReport: null }; } catch (error) { logger.error("unlink-window failed:", error); return { success: false as const, error: error instanceof Error ? error.message : "Failed to remove season", syncReport: null, }; } } return { success: false as const, error: "Invalid intent", syncReport: null, }; } export default function AdminTournamentDetail({ loaderData, actionData, }: Route.ComponentProps) { const { tournament, results, canonicalParticipants, linkedSportsSeasons, primaryEvent, bracketMajor, isCs2, } = loaderData; const retryFetcher = useFetcher(); const primaryFetcher = useFetcher(); const unlinkFetcher = useFetcher(); // For bracket majors, scoring lives on the primary window's bracket/stage UI. const primaryScoringHref = bracketMajor && primaryEvent ? isCs2 ? `/admin/sports-seasons/${primaryEvent.sportsSeasonId}/events/${primaryEvent.id}/cs2-setup` : `/admin/sports-seasons/${primaryEvent.sportsSeasonId}/events/${primaryEvent.id}/bracket` : null; // Prefer the latest action/retry response for the sync report const liveReport: SyncReport | null = (retryFetcher.data?.syncReport ?? actionData?.syncReport) ?? null; const participantById = new Map( canonicalParticipants.map((p) => [p.id, p]) ); return (

{tournament.name}

{tournament.status}
{primaryScoringHref && ( )}

{tournament.year} {tournament.location ? ` — ${tournament.location}` : ""} {tournament.surface ? ` — ${tournament.surface}` : ""}

{liveReport && ( Synced to {liveReport.windowsSynced}{" "} {liveReport.windowsSynced === 1 ? "window" : "windows"} Canonical results were fanned out to every linked scoring window. {liveReport.failures.length > 0 && (
{liveReport.failures.length}{" "} {liveReport.failures.length === 1 ? "window" : "windows"}{" "} failed to sync
{liveReport.failures.map((f) => (
event: {f.scoringEventId}
season: {f.sportsSeasonId}
{f.error}
))}
)}
)} Linked Sports Seasons {linkedSportsSeasons.length}{" "} {linkedSportsSeasons.length === 1 ? "sports season" : "sports seasons"}{" "} use this tournament as a scoring event {linkedSportsSeasons.length > 0 ? (
{linkedSportsSeasons.map((link) => (

{link.sportsSeason.sport.name} -{" "} {link.sportsSeason.name}

{link.sportsSeason.year} •{" "} {link.sportsSeason.scoringType.replace("_", " ")}

{bracketMajor && (link.isPrimary ? ( Primary ) : ( ))} {link.sportsSeason.status} Remove {link.sportsSeason.sport.name} - {link.sportsSeason.name}? This deletes that season's scoring event (and its results) but keeps{" "} {tournament.name} and the other linked seasons. {link.isPrimary && linkedSportsSeasons.length > 1 && ( <> Because this is the primary scoring window, another season will be promoted to primary automatically. )} {linkedSportsSeasons.length === 1 && ( <> This is the only linked season — the tournament will remain but have no windows until you link one again. )} Cancel Remove
))}
) : (

No sports seasons are using this tournament yet.

)}
Current Results {results.length}{" "} {results.length === 1 ? "result" : "results"} recorded {results.length === 0 ? (

No results recorded yet. Paste a ranked list to the right to import.

) : ( Placement Participant {results.map((r) => { const p = participantById.get(r.participantId); return ( {r.placement ?? "—"} {p?.name ?? ( {r.participantId} )} ); })}
)}
{bracketMajor ? ( Scoring This is a bracket major. Results are derived from the bracket/stage workflow on the primary window and fan out here automatically. {primaryScoringHref ? ( ) : (

No primary window is set up yet for this tournament.

)}
) : ( ({ id: p.id, name: p.name, }))} sportsSeasonId="" existingResultParticipantIds={ new Set(results.map((r) => r.participantId)) } intent="batch-upsert-results" /> )}
); }