import { Form, Link, redirect, useNavigate, useNavigation } from "react-router"; import { auth } from "~/lib/auth.server"; import { isUserAdmin } from "~/models/user"; import type { Route } from "./+types/admin.sports-seasons.$id"; import { logger } from "~/lib/logger"; import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season"; import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator"; import { createDailySnapshot } from "~/models/standings"; import { database } from "~/database/context"; import { participantEvSnapshots, seasonSports } from "~/database/schema"; import { eq, desc } from "drizzle-orm"; import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry"; import { runSportsSeasonSimulation } from "~/services/simulations/runner"; import { syncStandings } from "~/services/standings-sync/index"; import { getPendingStandingsMappings, deletePendingStandingsMapping, } from "~/models/pending-standings-mappings"; import { findParticipantById, findParticipantsBySportsSeasonId, updateParticipant, } from "~/models/season-participant"; import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings"; import { getLastSeasonResultsSyncedAt, upsertParticipantSeasonResult } from "~/models/participant-season-result"; import { buildUnmatchedTeamResolutionView, type MatchConfidence } from "~/lib/unmatched-team-reconciliation"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; 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 { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "~/components/ui/select"; import { Trash2, Users, Trophy, Calculator, CheckCircle2, Zap, AlertTriangle, Loader2, RefreshCw, Copy } from "lucide-react"; import { useState } from "react"; const SELECT_CLASS = "h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"; type PendingStandingData = { teamName?: string; wins?: number; losses?: number; otLosses?: number | null; ties?: number | null; gamesPlayed?: number; conference?: string | null; division?: string | null; leagueRank?: number; streak?: string | null; lastTen?: string | null; }; function getConfidenceBadgeVariant(confidence: MatchConfidence) { switch (confidence) { case "exact": return "bg-emerald-500/15 text-emerald-400 border-emerald-500/30"; case "partial": return "bg-sky-500/15 text-sky-400 border-sky-500/30"; case "review": return "bg-amber-500/15 text-amber-500 border-amber-500/30"; default: return "bg-muted text-muted-foreground border-border"; } } function getConfidenceLabel(confidence: MatchConfidence) { switch (confidence) { case "exact": return "Exact match"; case "partial": return "Partial match"; case "review": return "Needs review"; default: return "No suggestion"; } } function formatTeamRecord(standingData: PendingStandingData) { const wins = standingData.wins ?? 0; const losses = standingData.losses ?? 0; if (typeof standingData.otLosses === "number") { return `${wins}-${losses}-${standingData.otLosses}`; } if (typeof standingData.ties === "number") { return `${wins}-${losses}-${standingData.ties}`; } return `${wins}-${losses}`; } export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) { throw new Response("Sports season not found", { status: 404 }); } const participants = await findParticipantsBySportsSeasonId(params.id); // Get the most recent snapshot date (if any) const db = database(); const lastSnapshot = await db .select({ snapshotDate: participantEvSnapshots.snapshotDate }) .from(participantEvSnapshots) .where(eq(participantEvSnapshots.sportsSeasonId, params.id)) .orderBy(desc(participantEvSnapshots.snapshotDate)) .limit(1); const lastSimulatedDate = lastSnapshot[0]?.snapshotDate ?? null; const simulatorInfo = sportsSeason.sport?.simulatorType ? getSimulatorInfo(sportsSeason.sport.simulatorType as SimulatorType) : null; const lastStandingsSyncedAt = sportsSeason.sport?.type === "team" ? await getLastSyncedAt(params.id) : sportsSeason.sport?.type === "individual" ? await getLastSeasonResultsSyncedAt(params.id) : null; const pendingMappings = await getPendingStandingsMappings(params.id); return { sportsSeason, participants, lastSimulatedDate, simulatorInfo, lastStandingsSyncedAt: lastStandingsSyncedAt?.toISOString() ?? null, pendingMappings, }; } export async function action(args: Route.ActionArgs) { const { request, params } = args; const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; const isAdmin = userId ? await isUserAdmin(userId) : false; if (!isAdmin) { throw new Response("Forbidden", { status: 403 }); } const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "delete") { await deleteSportsSeason(params.id); return redirect("/admin/sports-seasons"); } if (intent === "rescore") { try { const db = database(); const links = await db.query.seasonSports.findMany({ where: eq(seasonSports.sportsSeasonId, params.id), }); if (links.length === 0) { return { success: true, intent: "rescore", message: "No linked fantasy seasons found β€” nothing to rescore." }; } await Promise.all(links.map((link) => recalculateStandings(link.seasonId, db))); await Promise.all(links.map((link) => createDailySnapshot(link.seasonId, db))); return { success: true, intent: "rescore", message: `Rescored ${links.length} linked season(s).` }; } catch (error) { logger.error("Error rescoring:", error); return { error: "Failed to rescore. Please try again." }; } } if (intent === "sync-standings") { try { const result = await syncStandings(params.id); return { success: true, intent: "sync-standings", syncResult: result }; } catch (error) { logger.error("Error syncing standings:", error); return { syncError: error instanceof Error ? error.message : "Failed to sync standings. Please try again.", }; } } if (intent === "resolve-mapping") { const externalTeamId = formData.get("externalTeamId"); const participantId = formData.get("participantId"); const standingDataRaw = formData.get("standingData"); if ( typeof externalTeamId !== "string" || !externalTeamId.trim() || typeof participantId !== "string" || !participantId.trim() || typeof standingDataRaw !== "string" ) { return { error: "Invalid resolve-mapping payload." }; } try { const standingData = JSON.parse(standingDataRaw) as Record; const [participant, sportsSeason] = await Promise.all([ findParticipantById(participantId), findSportsSeasonById(params.id), ]); if (!participant || participant.sportsSeasonId !== params.id) { return { error: "Selected participant does not belong to this sports season." }; } // Write externalId onto the participant for future ID-first matching await updateParticipant(participantId, { externalId: externalTeamId }); // Route upsert to the correct table based on sport type if (sportsSeason?.scoringPattern === "season_standings") { await upsertParticipantSeasonResult({ participantId, sportsSeasonId: params.id, currentPoints: (standingData.currentPoints as number) ?? 0, currentPosition: (standingData.leagueRank as number) ?? null, }); } else { await upsertRegularSeasonStandings([ { participantId, sportsSeasonId: params.id, wins: (standingData.wins as number) ?? 0, losses: (standingData.losses as number) ?? 0, otLosses: (standingData.otLosses as number | null) ?? null, ties: (standingData.ties as number | null) ?? null, winPct: (standingData.winPct as number) ?? 0, gamesPlayed: (standingData.gamesPlayed as number) ?? 0, gamesBack: (standingData.gamesBack as number | null) ?? null, conference: (standingData.conference as string | null) ?? null, division: (standingData.division as string | null) ?? null, conferenceRank: (standingData.conferenceRank as number | null) ?? null, divisionRank: (standingData.divisionRank as number | null) ?? null, leagueRank: (standingData.leagueRank as number) ?? 0, streak: (standingData.streak as string | null) ?? null, lastTen: (standingData.lastTen as string | null) ?? null, homeRecord: (standingData.homeRecord as string | null) ?? null, awayRecord: (standingData.awayRecord as string | null) ?? null, externalTeamId, syncedAt: new Date(), }, ]); } // Remove from pending queue await deletePendingStandingsMapping(params.id, externalTeamId); return { success: true, intent: "resolve-mapping", resolvedTeam: String(standingData.teamName ?? ""), resolvedParticipant: participant?.name ?? "", }; } catch (error) { logger.error("Error resolving mapping:", error); return { error: "Failed to resolve mapping. Please try again." }; } } if (intent === "finalize-standings") { try { await processSeasonStandings(params.id); await updateSportsSeason(params.id, { status: "completed" }); return { success: true, intent: "finalize-standings", message: "Standings finalized and fantasy placements assigned!" }; } catch (error) { logger.error("Error finalizing standings:", error); return { error: "Failed to finalize standings. Please try again." }; } } if (intent === "simulate") { try { await runSportsSeasonSimulation(params.id); return redirect(`/admin/sports-seasons/${params.id}/expected-values`); } catch (error) { return { simulateError: error instanceof Error ? error.message : "Simulation failed", }; } } // Update const name = formData.get("name"); const year = formData.get("year"); const startDate = formData.get("startDate"); const endDate = formData.get("endDate"); const status = formData.get("status"); const scoringType = formData.get("scoringType"); const scoringPattern = formData.get("scoringPattern"); const totalMajors = formData.get("totalMajors"); const draftOn = formData.get("draftOn"); const draftOff = formData.get("draftOff"); const externalSeasonId = formData.get("externalSeasonId"); // Validation if (typeof name !== "string" || !name.trim()) { return { error: "Season name is required" }; } if (typeof year !== "string") { return { error: "Year is required" }; } const yearNum = parseInt(year, 10); if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) { return { error: "Year must be between 2000 and 2100" }; } if (status !== "upcoming" && status !== "active" && status !== "completed") { return { error: "Invalid status" }; } if (scoringType !== "playoffs" && scoringType !== "regular_season" && scoringType !== "majors") { return { error: "Invalid scoring type" }; } const validScoringPatterns = ["playoff_bracket", "season_standings", "qualifying_points"]; if (scoringPattern && typeof scoringPattern === "string" && !validScoringPatterns.includes(scoringPattern)) { return { error: "Invalid scoring pattern" }; } if (typeof draftOn !== "string" || !draftOn) { return { error: "Draft open date is required" }; } if (typeof draftOff !== "string" || !draftOff) { return { error: "Draft close date is required" }; } if (draftOff < draftOn) { return { error: "Draft close date must be on or after draft open date" }; } try { const updateData: Partial = { name: name.trim(), year: yearNum, startDate: typeof startDate === "string" && startDate ? startDate : null, endDate: typeof endDate === "string" && endDate ? endDate : null, status, scoringType, draftOn, draftOff, }; if (scoringPattern && typeof scoringPattern === "string") { updateData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"; } if (totalMajors && typeof totalMajors === "string") { const totalMajorsNum = parseInt(totalMajors, 10); if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) { updateData.totalMajors = totalMajorsNum; } } updateData.externalSeasonId = typeof externalSeasonId === "string" && externalSeasonId.trim() ? externalSeasonId.trim() : null; await updateSportsSeason(params.id, updateData); return { success: true, message: "Sports season updated successfully!" }; } catch (error) { logger.error("Error updating sports season:", error); return { error: "Failed to update sports season. Please try again." }; } } export default function EditSportsSeason({ loaderData, actionData }: Route.ComponentProps) { const { sportsSeason, participants, lastSimulatedDate, simulatorInfo, lastStandingsSyncedAt, pendingMappings } = loaderData; const navigate = useNavigate(); const navigation = useNavigation(); const isSyncingStandings = navigation.state === "submitting" && (navigation.formData?.get("intent") as string) === "sync-standings"; const [scoringPattern, setScoringPattern] = useState(sportsSeason.scoringPattern || ""); const pendingMappingsWithSuggestions = pendingMappings.map((mapping) => ({ ...mapping, standingData: (mapping.standingData ?? {}) as PendingStandingData, resolutionView: buildUnmatchedTeamResolutionView(mapping.teamName, participants), })); return (

Edit Sports Season

{sportsSeason.sport.name} - {sportsSeason.name}

Sports Season Details Update the information for this sports season

Playoffs: Team sports playoffs. Regular Season: Full season standings. Majors: Individual sport majors.

Qualifying Points: For sports like Golf/Tennis where participants earn points across majors, then top 8 get fantasy points.

{scoringPattern === "qualifying_points" && (

How many major tournaments will be tracked? (e.g., Golf has 4 majors, Tennis has 4 Grand Slams)

)}

This season appears in league creation and pre-draft settings only between these two dates (inclusive).

Used by the match sync cron job. For CS2: PandaScore serie_id. For MLB/NBA: year (e.g., 2025).

{actionData?.error && (
{actionData.error}
)} {actionData?.success && (
{actionData.message}
)}
Participants {participants.length} {participants.length === 1 ? "participant" : "participants"}
{participants.length === 0 ? (

No participants added yet. Add teams or players to this season.

) : (
{participants.slice(0, 5).map((participant) => (
{participant.name}
))} {participants.length > 5 && (

And {participants.length - 5} more...

)}
)}
Expected Values {simulatorInfo ? simulatorInfo.name : "Manage probability distributions and projected points"}
{sportsSeason.simulationStatus === "failed" && ( Last run failed )} {sportsSeason.sport?.simulatorType === "tennis_qualifying_points" && ( )} {sportsSeason.sport?.simulatorType === "golf_qualifying_points" && ( )} {simulatorInfo && (
)}

{simulatorInfo ? <> {simulatorInfo.description}.{" "} {lastSimulatedDate ? `Last simulated: ${lastSimulatedDate}.` : "No simulation has been run yet."} {" "}Import futures odds first, then run the simulation to update EVs and save a snapshot. : "Import futures odds to set probability distributions."}

{"simulateError" in (actionData ?? {}) && actionData?.simulateError && (
{actionData.simulateError}
)}
Scoring Events Manage games, tournaments, and schedule entries

Create playoff brackets, major tournaments, or import a race/game schedule.

{sportsSeason.sport?.type === "team" && (
Regular Season Standings Sync current W/L standings from the official API, or edit manually. {lastStandingsSyncedAt && ( Last synced: {new Date(lastStandingsSyncedAt).toLocaleString()}. )}
{(actionData?.success && actionData.intent === "sync-standings") || actionData?.syncError ? ( {actionData?.success && actionData.intent === "sync-standings" && actionData.syncResult && (
Synced {actionData.syncResult.synced} team{actionData.syncResult.synced !== 1 ? "s" : ""} successfully.
{actionData.syncResult.unmatched.length > 0 && (

{actionData.syncResult.unmatched.length} team{actionData.syncResult.unmatched.length !== 1 ? "s" : ""} could not be matched to participants:

    {actionData.syncResult.unmatched.map((u: { teamName: string; externalTeamId: string }) => (
  • {u.teamName}
  • ))}

Use the "Unmatched Teams" card below to assign these to participants. Future syncs will use the saved ID.

)}
)} {actionData?.syncError && (
{actionData.syncError}
)}
) : null}
)} {sportsSeason.sport?.type === "individual" && (
Championship Standings Sync current standings from the official API, or edit manually. {lastStandingsSyncedAt && ( Last synced: {new Date(lastStandingsSyncedAt).toLocaleString()}. )}
{(actionData?.success && actionData.intent === "sync-standings") || actionData?.syncError ? ( {actionData?.success && actionData.intent === "sync-standings" && actionData.syncResult && (
Synced {actionData.syncResult.synced} driver{actionData.syncResult.synced !== 1 ? "s" : ""} successfully.
{actionData.syncResult.unmatched.length > 0 && (
{actionData.syncResult.unmatched.length} driver{actionData.syncResult.unmatched.length !== 1 ? "s" : ""} could not be matched β€” see the "Unmatched Drivers" card below to resolve.
)}
)} {actionData?.syncError && (
{actionData.syncError}
)}
) : null}
)} {pendingMappings.length > 0 && ( {sportsSeason.sport?.type === "individual" ? "Unmatched Drivers" : "Unmatched Teams"} ({pendingMappings.length}) {sportsSeason.sport?.type === "individual" ? "These drivers came back from the official standings feed but could not be tied to a participant in this season." : "These teams came back from the official standings feed but could not be tied to a participant in this season." }{" "} Use the standings details and suggested participant to confirm the right match. Once resolved, future syncs will reuse the saved external ID.
Exact match Partial match Needs review
{actionData?.success && actionData.intent === "resolve-mapping" && (
Resolved "{actionData.resolvedTeam}" to "{actionData.resolvedParticipant}"
)} {pendingMappingsWithSuggestions.map((mapping) => ( (() => { const topCandidateIds = new Set( mapping.resolutionView.topCandidates.map((candidate) => candidate.participantId) ); const remainingParticipants = mapping.resolutionView.orderedParticipants.filter( (participant) => !topCandidateIds.has(participant.id) ); return (

{mapping.teamName}

{getConfidenceLabel(mapping.resolutionView.confidence)}

Feed ID: {mapping.externalTeamId}

{typeof mapping.standingData.leagueRank === "number" && ( League rank #{mapping.standingData.leagueRank} )} {(mapping.standingData.conference || mapping.standingData.division) && ( {[mapping.standingData.conference, mapping.standingData.division].filter(Boolean).join(" - ")} )} {typeof mapping.standingData.wins === "number" && typeof mapping.standingData.losses === "number" && ( Record {formatTeamRecord(mapping.standingData)} )} {typeof mapping.standingData.gamesPlayed === "number" && ( {mapping.standingData.gamesPlayed} GP )} {mapping.standingData.streak && ( Streak {mapping.standingData.streak} )} {mapping.standingData.lastTen && ( Last 10 {mapping.standingData.lastTen} )}

No participant name matched this API team.

Suggested participant

{getConfidenceLabel(mapping.resolutionView.confidence)}

{mapping.resolutionView.suggestedParticipantName ?? "No strong suggestion"}

{mapping.resolutionView.topCandidates.length > 0 && (
Top matches:{" "} {mapping.resolutionView.topCandidates .map((candidate) => candidate.participantName) .join(", ")}
)}
); })() ))}
)} {sportsSeason.scoringPattern === "season_standings" && (
Championship Standings Update participant positions and points as the season progresses
)} {sportsSeason.scoringPattern === "season_standings" && (
Finalize Standings {sportsSeason.status === "completed" && ( Finalized )} {sportsSeason.status === "completed" ? "Season standings have been finalized and fantasy placements assigned." : "When the championship is over, finalize standings to assign fantasy placements (1st–8th) to participants based on their final positions."}
{sportsSeason.status !== "completed" && ( {actionData?.error && (
{actionData.error}
)} {actionData?.success && actionData.intent === "finalize-standings" && (
{actionData.message}
)} Finalize championship standings? This will read the current participant standings and assign fantasy placements (1st through 8th place). The season will be marked as completed. This action can be re-run to correct results if needed. Cancel
Finalize Standings
)}
)} Fantasy Standings Force a recalculation of all fantasy standings linked to this sports season. Use this after fixing scoring bugs or data corrections. {actionData?.success && actionData.intent === "rescore" && (
{actionData.message}
)}
Danger Zone Permanently delete this sports season Are you absolutely sure? This will permanently delete the sports season "{sportsSeason.name}" and all associated participants and results. This action cannot be undone. Cancel
Delete
); }