From dc879e3033b26dd620de0c4a8c7e238845384ae8 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:07:18 -0400 Subject: [PATCH] feat: sort admin sports seasons by status, sport name, then year (#286) Active seasons sort first, then upcoming, then completed. Within each status group, seasons sort by sport name alphabetically then year ascending. Fixes return type of findAllSportsSeasons to reflect the included sport/participants relations, removing the cast in the loader. Co-authored-by: Claude Sonnet 4.6 --- app/models/__tests__/sports-season.test.ts | 39 ++++++++++++++++++++-- app/models/sports-season.ts | 18 ++++++++-- app/routes/admin.sports-seasons.tsx | 10 +----- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/app/models/__tests__/sports-season.test.ts b/app/models/__tests__/sports-season.test.ts index 4bbabd1..34c71ff 100644 --- a/app/models/__tests__/sports-season.test.ts +++ b/app/models/__tests__/sports-season.test.ts @@ -36,9 +36,9 @@ const pastStart = "2020-01-01"; const pastEnd = "2020-12-31"; const mockSeasons = [ - { id: "season-1", name: "2025 NBA Playoffs", draftOn: today, draftOff: future, year: 2025 }, - { id: "season-2", name: "2025 F1 Season", draftOn: today, draftOff: future, year: 2025 }, - { id: "season-3", name: "2024 Old Golf", draftOn: pastStart, draftOff: pastEnd, year: 2024 }, + { id: "season-1", name: "2025 NBA Playoffs", draftOn: today, draftOff: future, year: 2025, status: "upcoming", sport: { name: "NBA" } }, + { id: "season-2", name: "2025 F1 Season", draftOn: today, draftOff: future, year: 2025, status: "upcoming", sport: { name: "F1" } }, + { id: "season-3", name: "2024 Old Golf", draftOn: pastStart, draftOff: pastEnd, year: 2024, status: "completed", sport: { name: "Golf" } }, ]; function makeMockDb(seasons: typeof mockSeasons) { @@ -61,6 +61,39 @@ describe("findAllSportsSeasons", () => { const result = await findAllSportsSeasons(); expect(result).toHaveLength(3); }); + + it("sorts active before upcoming before completed", async () => { + const seasons = [ + { id: "s1", name: "Completed", draftOn: pastStart, draftOff: pastEnd, year: 2024, status: "completed", sport: { name: "Sport" } }, + { id: "s2", name: "Active", draftOn: today, draftOff: future, year: 2025, status: "active", sport: { name: "Sport" } }, + { id: "s3", name: "Upcoming", draftOn: today, draftOff: future, year: 2026, status: "upcoming", sport: { name: "Sport" } }, + ]; + vi.mocked(database).mockReturnValue(makeMockDb(seasons) as never); + const result = await findAllSportsSeasons(); + expect(result.map((s) => s.status)).toEqual(["active", "upcoming", "completed"]); + }); + + it("sorts by sport name alphabetically within the same status", async () => { + const seasons = [ + { id: "s1", name: "Zebra Season", draftOn: today, draftOff: future, year: 2025, status: "active", sport: { name: "Zebra" } }, + { id: "s2", name: "Apple Season", draftOn: today, draftOff: future, year: 2025, status: "active", sport: { name: "Apple" } }, + { id: "s3", name: "Mango Season", draftOn: today, draftOff: future, year: 2025, status: "active", sport: { name: "Mango" } }, + ]; + vi.mocked(database).mockReturnValue(makeMockDb(seasons) as never); + const result = await findAllSportsSeasons(); + expect(result.map((s) => s.sport.name)).toEqual(["Apple", "Mango", "Zebra"]); + }); + + it("sorts by year ascending within the same status and sport name", async () => { + const seasons = [ + { id: "s1", name: "NFL 2026", draftOn: today, draftOff: future, year: 2026, status: "upcoming", sport: { name: "NFL" } }, + { id: "s2", name: "NFL 2024", draftOn: today, draftOff: future, year: 2024, status: "upcoming", sport: { name: "NFL" } }, + { id: "s3", name: "NFL 2025", draftOn: today, draftOff: future, year: 2025, status: "upcoming", sport: { name: "NFL" } }, + ]; + vi.mocked(database).mockReturnValue(makeMockDb(seasons) as never); + const result = await findAllSportsSeasons(); + expect(result.map((s) => s.year)).toEqual([2024, 2025, 2026]); + }); }); describe("findDraftableSportsSeasons", () => { diff --git a/app/models/sports-season.ts b/app/models/sports-season.ts index c3633e1..6a5a9c8 100644 --- a/app/models/sports-season.ts +++ b/app/models/sports-season.ts @@ -15,6 +15,9 @@ export type NewSportsSeason = typeof schema.sportsSeasons.$inferInsert; export type SportsSeasonWithSport = SportsSeason & { sport: typeof schema.sports.$inferSelect; }; +export type SportsSeasonListItem = SportsSeasonWithSport & { + participants: Array<{ id: string }>; +}; export type SportsSeasonStatus = "upcoming" | "active" | "completed"; export type ScoringType = "playoffs" | "regular_season" | "majors"; @@ -85,10 +88,9 @@ export async function findSportsSeasonsByStatus(status: SportsSeasonStatus): Pro }); } -export async function findAllSportsSeasons(): Promise { +export async function findAllSportsSeasons(): Promise { const db = database(); - return await db.query.sportsSeasons.findMany({ - orderBy: (sportsSeasons, { desc, asc }) => [desc(sportsSeasons.year), asc(sportsSeasons.name)], + const results = await db.query.sportsSeasons.findMany({ with: { sport: true, participants: { @@ -98,6 +100,16 @@ export async function findAllSportsSeasons(): Promise { }, }, }); + + const statusOrder: Record = { active: 0, upcoming: 1, completed: 2 }; + + return results.sort((a, b) => { + const statusDiff = statusOrder[a.status] - statusOrder[b.status]; + if (statusDiff !== 0) return statusDiff; + const sportNameDiff = a.sport.name.localeCompare(b.sport.name); + if (sportNameDiff !== 0) return sportNameDiff; + return a.year - b.year; + }); } export async function findDraftableSportsSeasons(): Promise { diff --git a/app/routes/admin.sports-seasons.tsx b/app/routes/admin.sports-seasons.tsx index 2f4d925..95c0000 100644 --- a/app/routes/admin.sports-seasons.tsx +++ b/app/routes/admin.sports-seasons.tsx @@ -27,15 +27,7 @@ export function meta(): Route.MetaDescriptors { export async function loader() { const sportsSeasons = await findAllSportsSeasons(); - // Type assertion since we know the sport and participants relations are included from the model - return { - sportsSeasons: sportsSeasons as Array< - typeof sportsSeasons[0] & { - sport: { id: string; name: string; type: string; slug: string }; - participants: Array<{ id: string }>; - } - > - }; + return { sportsSeasons }; } export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) {