import { Form, Link } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.regular-standings"; import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; import { getRegularSeasonStandings, upsertManualStanding } from "~/models/regular-season-standings"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { ArrowLeft, Save, BarChart3 } from "lucide-react"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Regular Season Standings — ${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); const standingsRecords = await getRegularSeasonStandings(params.id); // Build a map of existing records keyed by participantId const standingsMap = Object.fromEntries( standingsRecords.map((r) => [r.participantId, r]) ); return { sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; simulatorType: string | null } }, participants, standingsMap, }; } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); // Parse updates from form fields: wins_, losses_, ties_, otLosses_, etc. const byId = new Map< string, { wins?: number; losses?: number; otLosses?: number | null; ties?: number | null; gamesPlayed?: number | null; leagueRank?: number | null; tablePoints?: number | null; goalsFor?: number | null; goalsAgainst?: number | null; goalDifference?: number | null; conference?: string | null; division?: string | null; } >(); for (const [key, value] of formData.entries()) { const winsMatch = key.match(/^wins_(.+)$/); const lossesMatch = key.match(/^losses_(.+)$/); const otMatch = key.match(/^otLosses_(.+)$/); const tiesMatch = key.match(/^ties_(.+)$/); const gpMatch = key.match(/^gamesPlayed_(.+)$/); const rankMatch = key.match(/^leagueRank_(.+)$/); const pointsMatch = key.match(/^tablePoints_(.+)$/); const gfMatch = key.match(/^goalsFor_(.+)$/); const gaMatch = key.match(/^goalsAgainst_(.+)$/); const gdMatch = key.match(/^goalDifference_(.+)$/); const confMatch = key.match(/^conference_(.+)$/); const divMatch = key.match(/^division_(.+)$/); if (winsMatch) { const id = winsMatch[1]; const wins = parseInt(value as string, 10); if (!isNaN(wins)) byId.set(id, { ...byId.get(id), wins }); } else if (lossesMatch) { const id = lossesMatch[1]; const losses = parseInt(value as string, 10); if (!isNaN(losses)) byId.set(id, { ...byId.get(id), losses }); } else if (otMatch) { const id = otMatch[1]; const otLosses = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), otLosses: isNaN(otLosses as number) ? null : otLosses }); } else if (tiesMatch) { const id = tiesMatch[1]; const ties = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), ties: isNaN(ties as number) ? null : ties }); } else if (gpMatch) { const id = gpMatch[1]; const gamesPlayed = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), gamesPlayed: isNaN(gamesPlayed as number) ? null : gamesPlayed }); } else if (rankMatch) { const id = rankMatch[1]; const leagueRank = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), leagueRank: isNaN(leagueRank as number) ? null : leagueRank }); } else if (pointsMatch) { const id = pointsMatch[1]; const tablePoints = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), tablePoints: isNaN(tablePoints as number) ? null : tablePoints }); } else if (gfMatch) { const id = gfMatch[1]; const goalsFor = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), goalsFor: isNaN(goalsFor as number) ? null : goalsFor }); } else if (gaMatch) { const id = gaMatch[1]; const goalsAgainst = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), goalsAgainst: isNaN(goalsAgainst as number) ? null : goalsAgainst }); } else if (gdMatch) { const id = gdMatch[1]; const goalDifference = value === "" ? null : parseInt(value as string, 10); byId.set(id, { ...byId.get(id), goalDifference: isNaN(goalDifference as number) ? null : goalDifference }); } else if (confMatch) { const id = confMatch[1]; byId.set(id, { ...byId.get(id), conference: (value as string).trim() || null }); } else if (divMatch) { const id = divMatch[1]; byId.set(id, { ...byId.get(id), division: (value as string).trim() || null }); } } try { for (const [participantId, data] of byId.entries()) { if (data.wins === null && data.losses === null) continue; const wins = data.wins ?? 0; const losses = data.losses ?? 0; const ties = data.ties ?? 0; const gamesPlayed = data.gamesPlayed ?? wins + losses + ties + (data.otLosses ?? 0); const winPct = gamesPlayed > 0 ? wins / gamesPlayed : 0; const goalsFor = data.goalsFor ?? null; const goalsAgainst = data.goalsAgainst ?? null; const goalDifference = data.goalDifference ?? (goalsFor !== null && goalsAgainst !== null ? goalsFor - goalsAgainst : null); const tablePoints = data.tablePoints ?? wins * 3 + ties; await upsertManualStanding(participantId, params.id, { wins, losses, otLosses: data.otLosses ?? null, ties: data.ties ?? null, tablePoints, goalsFor, goalsAgainst, goalDifference, gamesPlayed, leagueRank: data.leagueRank ?? null, winPct, conference: data.conference ?? null, division: data.division ?? null, }); } return { success: true }; } catch (error) { logger.error("Error saving regular season standings:", error); return { error: "Failed to save standings. Please try again." }; } } export default function ManageRegularStandings({ loaderData, actionData }: Route.ComponentProps) { const { sportsSeason, participants, standingsMap } = loaderData; const isNhl = sportsSeason.sport?.name?.toLowerCase().includes("nhl") || sportsSeason.sport?.name?.toLowerCase().includes("hockey"); const isEpl = sportsSeason.sport?.simulatorType === "epl_standings"; // Sort participants: those with records first (by wins desc), then unrecorded const sorted = [...participants].toSorted((a, b) => { const recA = standingsMap[a.id]; const recB = standingsMap[b.id]; if (recA && recB) { if (isEpl) return (recA.leagueRank ?? 999) - (recB.leagueRank ?? 999); return (recB.wins ?? 0) - (recA.wins ?? 0); } if (recA) return -1; if (recB) return 1; return a.name.localeCompare(b.name); }); return (

Regular Season Standings

{sportsSeason.sport.name} — {sportsSeason.name}

Manually enter current standings. These will be overwritten on the next auto-sync unless you use this page again afterward.

{actionData?.error && (
{actionData.error}
)} {actionData?.success && (
Standings saved.
)} W/L Records {isEpl ? "Enter Premier League table rows. Points, GP, and GD are auto-derived if left blank." : <>Enter wins, losses{isNhl ? ", and OT losses" : ""} for each team. Conference and division are optional but improve the standings display.}
{isEpl ? (
Team Pos GP W D L GF GA GD Pts
) : (
Team W L {isNhl && OTL} Conference Division
)} {sorted.map((participant) => { const record = standingsMap[participant.id]; const isSynced = record && record.syncedAt !== null; if (isEpl) { return (
{participant.name} {isSynced && ( (synced) )}
); } return (
{participant.name} {isSynced && ( (synced) )} {isNhl && ( )}
); })}
); }