From ce0ed4f485269815fe294f3759fb125fffc5b4dd Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 12 May 2026 16:52:26 -0700 Subject: [PATCH] Hide completed seasons in admin tools (#418) --- app/routes/__tests__/admin.simulators.test.ts | 73 ++++++++++++++++++- .../admin.sports-seasons.helpers.test.ts | 22 ++++++ app/routes/admin.simulators.tsx | 4 +- app/routes/admin.sports-seasons.helpers.ts | 7 ++ app/routes/admin.sports-seasons.tsx | 45 ++++++++++-- .../simulations/__tests__/runner.test.ts | 9 +++ app/services/simulations/runner.ts | 3 + 7 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 app/routes/__tests__/admin.sports-seasons.helpers.test.ts create mode 100644 app/routes/admin.sports-seasons.helpers.ts diff --git a/app/routes/__tests__/admin.simulators.test.ts b/app/routes/__tests__/admin.simulators.test.ts index 476b845..625c060 100644 --- a/app/routes/__tests__/admin.simulators.test.ts +++ b/app/routes/__tests__/admin.simulators.test.ts @@ -8,7 +8,8 @@ vi.mock("~/services/simulations/runner", () => ({ runSportsSeasonSimulation: vi.fn(), })); -import { action } from "../admin.simulators"; +import { action, loader } from "../admin.simulators"; +import { listSportsSeasonSimulatorSummaries } from "~/models/simulator"; import { runSportsSeasonSimulation } from "~/services/simulations/runner"; function postForm(entries: Record) { @@ -31,6 +32,76 @@ beforeEach(() => { vi.clearAllMocks(); }); +describe("admin simulators loader", () => { + it("hides completed seasons", async () => { + vi.mocked(listSportsSeasonSimulatorSummaries).mockResolvedValue([ + { + sportsSeasonId: "season-upcoming", + seasonName: "2026", + year: 2026, + seasonStatus: "upcoming", + simulationStatus: "idle", + fantasySeasonId: null, + fantasySeasonName: null, + leagueName: null, + sportName: "NBA", + sportSlug: "nba", + simulatorType: "nba_bracket", + simulatorName: "NBA Bracket", + participantCount: 16, + participantInputCount: 16, + lastSimulatedDate: null, + readiness: { + status: "ready", + canRun: true, + participantInputCount: 16, + participantCount: 16, + missingInputs: [], + warnings: [], + }, + }, + { + sportsSeasonId: "season-completed", + seasonName: "2025", + year: 2025, + seasonStatus: "completed", + simulationStatus: "idle", + fantasySeasonId: null, + fantasySeasonName: null, + leagueName: null, + sportName: "NFL", + sportSlug: "nfl", + simulatorType: "nfl_bracket", + simulatorName: "NFL Bracket", + participantCount: 14, + participantInputCount: 14, + lastSimulatedDate: "2026-05-11", + readiness: { + status: "ready", + canRun: true, + participantInputCount: 14, + participantCount: 14, + missingInputs: [], + warnings: [], + }, + }, + ] as never); + + const response = await loader(); + + expect(response).toEqual({ + simulators: [ + expect.objectContaining({ + sportsSeasonId: "season-upcoming", + seasonStatus: "upcoming", + }), + ], + sports: ["NBA"], + simulatorTypes: ["nba_bracket"], + }); + }); +}); + describe("admin simulators action", () => { it("runs one simulator", async () => { vi.mocked(runSportsSeasonSimulation).mockResolvedValue({ diff --git a/app/routes/__tests__/admin.sports-seasons.helpers.test.ts b/app/routes/__tests__/admin.sports-seasons.helpers.test.ts new file mode 100644 index 0000000..5a92708 --- /dev/null +++ b/app/routes/__tests__/admin.sports-seasons.helpers.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { filterVisibleSportsSeasons } from "../admin.sports-seasons.helpers"; + +describe("filterVisibleSportsSeasons", () => { + const sportsSeasons = [ + { id: "season-1", status: "upcoming" }, + { id: "season-2", status: "active" }, + { id: "season-3", status: "completed" }, + ]; + + it("hides completed seasons when the filter is enabled", () => { + expect(filterVisibleSportsSeasons(sportsSeasons, true)).toEqual([ + { id: "season-1", status: "upcoming" }, + { id: "season-2", status: "active" }, + ]); + }); + + it("shows completed seasons when the filter is disabled", () => { + expect(filterVisibleSportsSeasons(sportsSeasons, false)).toEqual(sportsSeasons); + }); +}); diff --git a/app/routes/admin.simulators.tsx b/app/routes/admin.simulators.tsx index f0346ee..714d632 100644 --- a/app/routes/admin.simulators.tsx +++ b/app/routes/admin.simulators.tsx @@ -27,7 +27,8 @@ export function meta(): Route.MetaDescriptors { } export async function loader() { - const simulators = await listSportsSeasonSimulatorSummaries(); + const simulators = (await listSportsSeasonSimulatorSummaries()) + .filter((simulator) => simulator.seasonStatus !== "completed"); const sports = [...new Set(simulators.map((sim) => sim.sportName))].toSorted(); const simulatorTypes = [...new Set(simulators.map((sim) => sim.simulatorType))].toSorted(); return { simulators, sports, simulatorTypes }; @@ -159,7 +160,6 @@ export default function AdminSimulators({ loaderData }: Route.ComponentProps) { - diff --git a/app/routes/admin.sports-seasons.helpers.ts b/app/routes/admin.sports-seasons.helpers.ts new file mode 100644 index 0000000..e33ce7f --- /dev/null +++ b/app/routes/admin.sports-seasons.helpers.ts @@ -0,0 +1,7 @@ +export function filterVisibleSportsSeasons( + sportsSeasons: T[], + hideCompleted: boolean +): T[] { + if (!hideCompleted) return sportsSeasons; + return sportsSeasons.filter((season) => season.status !== "completed"); +} diff --git a/app/routes/admin.sports-seasons.tsx b/app/routes/admin.sports-seasons.tsx index 87ac14b..ad0bdda 100644 --- a/app/routes/admin.sports-seasons.tsx +++ b/app/routes/admin.sports-seasons.tsx @@ -1,7 +1,8 @@ -import { Link } from "react-router"; +import { Link, useSearchParams } from "react-router"; import type { Route } from "./+types/admin.sports-seasons"; import { findAllAdminSportsSeasons } from "~/models/sports-season"; +import { filterVisibleSportsSeasons } from "./admin.sports-seasons.helpers"; import { Card, CardContent, @@ -10,6 +11,8 @@ import { CardTitle, } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; +import { Label } from "~/components/ui/label"; +import { Switch } from "~/components/ui/switch"; import { Plus, Calendar } from "lucide-react"; import { Table, @@ -32,7 +35,17 @@ export async function loader() { export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) { const { sportsSeasons } = loaderData; + const [searchParams, setSearchParams] = useSearchParams(); const today = new Date().toISOString().slice(0, 10); + const hideCompleted = searchParams.get("showCompleted") !== "true"; + const visibleSportsSeasons = filterVisibleSportsSeasons(sportsSeasons, hideCompleted); + + function setHideCompleted(nextHideCompleted: boolean) { + const next = new URLSearchParams(searchParams); + if (nextHideCompleted) next.delete("showCompleted"); + else next.set("showCompleted", "true"); + setSearchParams(next); + } return (
@@ -53,10 +66,22 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) - All Sports Seasons - - {sportsSeasons.length} {sportsSeasons.length === 1 ? "season" : "seasons"} total - +
+
+ All Sports Seasons + + {visibleSportsSeasons.length} of {sportsSeasons.length} {sportsSeasons.length === 1 ? "season" : "seasons"} shown + +
+
+ + +
+
{sportsSeasons.length === 0 ? ( @@ -73,6 +98,14 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
+ ) : visibleSportsSeasons.length === 0 ? ( +
+ +

No visible sports seasons

+

+ All current sports seasons are completed. Turn off the filter to review them. +

+
) : ( @@ -88,7 +121,7 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) - {sportsSeasons.map((season) => ( + {visibleSportsSeasons.map((season) => ( {season.name} {season.sport.name} diff --git a/app/services/simulations/__tests__/runner.test.ts b/app/services/simulations/__tests__/runner.test.ts index deebd30..bdf543c 100644 --- a/app/services/simulations/__tests__/runner.test.ts +++ b/app/services/simulations/__tests__/runner.test.ts @@ -51,6 +51,7 @@ import { normalizeSimulationResultColumns } from "~/services/simulations/simulat const SEASON = { id: "season-1", + status: "active", simulationStatus: "idle", fantasySeasonId: null, }; @@ -139,6 +140,14 @@ describe("runSportsSeasonSimulation", () => { await expect(runSportsSeasonSimulation("season-1")).rejects.toThrow("already running"); }); + it("throws when the sports season is completed", async () => { + vi.mocked(findSportsSeasonById).mockResolvedValue({ ...SEASON, status: "completed" } as never); + + await expect(runSportsSeasonSimulation("season-1")).rejects.toThrow("cannot be simulated"); + expect(prepareSimulatorInputsForRun).not.toHaveBeenCalled(); + expect(updateSportsSeason).not.toHaveBeenCalled(); + }); + it("throws when readiness check fails", async () => { vi.mocked(validateSimulatorReadiness).mockResolvedValue({ canRun: false, diff --git a/app/services/simulations/runner.ts b/app/services/simulations/runner.ts index f75a907..beb8688 100644 --- a/app/services/simulations/runner.ts +++ b/app/services/simulations/runner.ts @@ -77,6 +77,9 @@ export async function runSportsSeasonSimulation( if (!sportsSeason) { throw new Error("Sports season not found"); } + if (sportsSeason.status === "completed") { + throw new Error("Completed sports seasons cannot be simulated."); + } const simulatorConfig = await getSportsSeasonSimulatorConfig(sportsSeasonId); if (!simulatorConfig) {