Hide completed seasons in admin tools (#418)
This commit is contained in:
parent
5b261bf258
commit
ce0ed4f485
7 changed files with 154 additions and 9 deletions
|
|
@ -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({
|
||||||
|
|
|
||||||
22
app/routes/__tests__/admin.sports-seasons.helpers.test.ts
Normal file
22
app/routes/__tests__/admin.sports-seasons.helpers.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
7
app/routes/admin.sports-seasons.helpers.ts
Normal file
7
app/routes/admin.sports-seasons.helpers.ts
Normal 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");
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue