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 { 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) : null; const pendingMappings = sportsSeason.sport?.type === "team" ? 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 = await findParticipantById(participantId); 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 }); // Upsert the standing record from the stored standingData 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"); // 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; } } 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).

{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 === "team" && pendingMappings.length > 0 && ( Unmatched Teams ({pendingMappings.length}) 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
); }