import { Form, Link, redirect, useNavigate, useNavigation } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import { isUserAdminByClerkId } 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 { syncStandings } from "~/services/standings-sync/index"; import { getPendingStandingsMappings, deletePendingStandingsMapping, } from "~/models/pending-standings-mappings"; import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant"; import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; import { Badge } from "~/components/ui/badge"; import { Checkbox } from "~/components/ui/checkbox"; import { Trash2, Users, Trophy, Calculator, CheckCircle2, Zap, AlertTriangle, Loader2, RefreshCw } from "lucide-react"; import { useState } from "react"; 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 { userId } = await getAuth(args); const isAdmin = userId ? await isUserAdminByClerkId(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; // 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 ?? "") }; } 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." }; } } // 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 isDraftable = formData.get("isDraftable") === "true"; // 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" }; } 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, isDraftable, }; 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 || ""); 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)

)}

When unchecked, this season will not appear in league creation or pre-draft league settings.

{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 )} {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."}

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 from the last sync could not be automatically matched to a participant. Assign each one to the correct participant to resolve. {actionData?.success && actionData.intent === "resolve-mapping" && (
Resolved: {actionData.resolvedTeam}
)} {pendingMappings.map((mapping) => (

{mapping.teamName}

ID: {mapping.externalTeamId}

))}
)} {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
); }