import { Link, useFetcher, Form } from "react-router"; import { useState } from "react"; 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 { linkTournamentToSportsSeason, unlinkTournamentFromSportsSeason, findSportsSeasonsByTournament, } from "~/models/sports-season-tournament"; import { findSportsSeasonsBySportId } from "~/models/sports-season"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { ArrowLeft, CheckCircle2, AlertTriangle, Plus, X } 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, allSportsSeasonsForSport] = await Promise.all([ getTournamentResults(tournament.id), findCanonicalParticipantsBySport(tournament.sportId), findSportsSeasonsByTournament(tournament.id), findSportsSeasonsBySportId(tournament.sportId), ]); return { tournament, results, canonicalParticipants, linkedSportsSeasons, allSportsSeasonsForSport }; } 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 === "add-sports-season") { const sportsSeasonId = formData.get("sportsSeasonId"); if (typeof sportsSeasonId !== "string" || !sportsSeasonId) { return { success: false as const, error: "Sports season ID is required", syncReport: null }; } try { await linkTournamentToSportsSeason(sportsSeasonId, tournament.id); return { success: true as const, error: null, syncReport: null }; } catch (error) { logger.error("Error linking sports season:", error); return { success: false as const, error: "Failed to link sports season. It may already be linked.", syncReport: null }; } } if (intent === "remove-sports-season") { const sportsSeasonId = formData.get("sportsSeasonId"); if (typeof sportsSeasonId !== "string" || !sportsSeasonId) { return { success: false as const, error: "Sports season ID is required", syncReport: null }; } try { await unlinkTournamentFromSportsSeason(sportsSeasonId, tournament.id); return { success: true as const, error: null, syncReport: null }; } catch (error) { logger.error("Error unlinking sports season:", error); return { success: false as const, error: "Failed to unlink sports season.", syncReport: null }; } } 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, }); } if (tournament.status !== "completed") { await updateTournamentStatus(tournament.id, "completed"); } const syncReport = await syncTournamentResults(tournament.id); 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); 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, }; } } return { success: false as const, error: "Invalid intent", syncReport: null, }; } export default function AdminTournamentDetail({ loaderData, actionData, }: Route.ComponentProps) { const { tournament, results, canonicalParticipants, linkedSportsSeasons, allSportsSeasonsForSport } = loaderData; const retryFetcher = useFetcher(); const [selectedSportsSeasonId, setSelectedSportsSeasonId] = useState(""); const linkedSeasonIds = new Set( linkedSportsSeasons.map((ls) => ls.sportsSeasonId) ); const availableSportsSeasons = allSportsSeasonsForSport.filter( (ss) => !linkedSeasonIds.has(ss.id) ); // 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}

{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"}{" "} linked to this tournament {actionData?.success && (actionData.error === null) && (
Updated successfully!
)} {actionData?.error && (
{actionData.error}
)} {linkedSportsSeasons.length > 0 && (
{linkedSportsSeasons.map((link) => (

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

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

{link.sportsSeason.status}
))}
)} {availableSportsSeasons.length > 0 ? (
) : ( linkedSportsSeasons.length > 0 ? (

All sports seasons for this sport have been linked

) : (

No sports seasons exist for this sport yet. Create one first.

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