Hide completed seasons in admin tools (#418)

This commit is contained in:
Chris Parsons 2026-05-12 16:52:26 -07:00 committed by GitHub
parent 5b261bf258
commit ce0ed4f485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 154 additions and 9 deletions

View file

@ -8,7 +8,8 @@ vi.mock("~/services/simulations/runner", () => ({
runSportsSeasonSimulation: vi.fn(), 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"; import { runSportsSeasonSimulation } from "~/services/simulations/runner";
function postForm(entries: Record<string, string | string[]>) { function postForm(entries: Record<string, string | string[]>) {
@ -31,6 +32,76 @@ beforeEach(() => {
vi.clearAllMocks(); 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", () => { describe("admin simulators action", () => {
it("runs one simulator", async () => { it("runs one simulator", async () => {
vi.mocked(runSportsSeasonSimulation).mockResolvedValue({ vi.mocked(runSportsSeasonSimulation).mockResolvedValue({

View file

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

View file

@ -27,7 +27,8 @@ export function meta(): Route.MetaDescriptors {
} }
export async function loader() { 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 sports = [...new Set(simulators.map((sim) => sim.sportName))].toSorted();
const simulatorTypes = [...new Set(simulators.map((sim) => sim.simulatorType))].toSorted(); const simulatorTypes = [...new Set(simulators.map((sim) => sim.simulatorType))].toSorted();
return { simulators, sports, simulatorTypes }; return { simulators, sports, simulatorTypes };
@ -159,7 +160,6 @@ export default function AdminSimulators({ loaderData }: Route.ComponentProps) {
<option value="all">All season statuses</option> <option value="all">All season statuses</option>
<option value="upcoming">Upcoming</option> <option value="upcoming">Upcoming</option>
<option value="active">Active</option> <option value="active">Active</option>
<option value="completed">Completed</option>
</select> </select>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -0,0 +1,7 @@
export function filterVisibleSportsSeasons<T extends { status: string }>(
sportsSeasons: T[],
hideCompleted: boolean
): T[] {
if (!hideCompleted) return sportsSeasons;
return sportsSeasons.filter((season) => season.status !== "completed");
}

View file

@ -1,7 +1,8 @@
import { Link } from "react-router"; import { Link, useSearchParams } from "react-router";
import type { Route } from "./+types/admin.sports-seasons"; import type { Route } from "./+types/admin.sports-seasons";
import { findAllAdminSportsSeasons } from "~/models/sports-season"; import { findAllAdminSportsSeasons } from "~/models/sports-season";
import { filterVisibleSportsSeasons } from "./admin.sports-seasons.helpers";
import { import {
Card, Card,
CardContent, CardContent,
@ -10,6 +11,8 @@ import {
CardTitle, CardTitle,
} from "~/components/ui/card"; } from "~/components/ui/card";
import { Button } from "~/components/ui/button"; 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 { Plus, Calendar } from "lucide-react";
import { import {
Table, Table,
@ -32,7 +35,17 @@ export async function loader() {
export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) { export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) {
const { sportsSeasons } = loaderData; const { sportsSeasons } = loaderData;
const [searchParams, setSearchParams] = useSearchParams();
const today = new Date().toISOString().slice(0, 10); 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 ( return (
<div className="p-8"> <div className="p-8">
@ -53,10 +66,22 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>All Sports Seasons</CardTitle> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<CardDescription> <div>
{sportsSeasons.length} {sportsSeasons.length === 1 ? "season" : "seasons"} total <CardTitle>All Sports Seasons</CardTitle>
</CardDescription> <CardDescription>
{visibleSportsSeasons.length} of {sportsSeasons.length} {sportsSeasons.length === 1 ? "season" : "seasons"} shown
</CardDescription>
</div>
<div className="flex items-center gap-3">
<Label htmlFor="hide-completed-seasons">Hide completed seasons</Label>
<Switch
id="hide-completed-seasons"
checked={hideCompleted}
onCheckedChange={setHideCompleted}
/>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{sportsSeasons.length === 0 ? ( {sportsSeasons.length === 0 ? (
@ -73,6 +98,14 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
</Link> </Link>
</Button> </Button>
</div> </div>
) : visibleSportsSeasons.length === 0 ? (
<div className="text-center py-12">
<Calendar className="mx-auto h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">No visible sports seasons</h3>
<p className="text-muted-foreground mt-2">
All current sports seasons are completed. Turn off the filter to review them.
</p>
</div>
) : ( ) : (
<Table> <Table>
<TableHeader> <TableHeader>
@ -88,7 +121,7 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{sportsSeasons.map((season) => ( {visibleSportsSeasons.map((season) => (
<TableRow key={season.id}> <TableRow key={season.id}>
<TableCell className="font-medium">{season.name}</TableCell> <TableCell className="font-medium">{season.name}</TableCell>
<TableCell>{season.sport.name}</TableCell> <TableCell>{season.sport.name}</TableCell>

View file

@ -51,6 +51,7 @@ import { normalizeSimulationResultColumns } from "~/services/simulations/simulat
const SEASON = { const SEASON = {
id: "season-1", id: "season-1",
status: "active",
simulationStatus: "idle", simulationStatus: "idle",
fantasySeasonId: null, fantasySeasonId: null,
}; };
@ -139,6 +140,14 @@ describe("runSportsSeasonSimulation", () => {
await expect(runSportsSeasonSimulation("season-1")).rejects.toThrow("already running"); 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 () => { it("throws when readiness check fails", async () => {
vi.mocked(validateSimulatorReadiness).mockResolvedValue({ vi.mocked(validateSimulatorReadiness).mockResolvedValue({
canRun: false, canRun: false,

View file

@ -77,6 +77,9 @@ export async function runSportsSeasonSimulation(
if (!sportsSeason) { if (!sportsSeason) {
throw new Error("Sports season not found"); 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); const simulatorConfig = await getSportsSeasonSimulatorConfig(sportsSeasonId);
if (!simulatorConfig) { if (!simulatorConfig) {