import { getAuth } from "@clerk/react-router/server"; import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId"; import { findLeagueById, isUserLeagueMember, isCommissioner, findTeamsBySeasonId, getUserDisplayName, } from "~/models"; import { getDraftPicks } from "~/models/draft-pick"; import { getSeasonResults } from "~/models/participant-season-result"; import { calculateBracketPoints } from "~/models/scoring-rules"; import { getQPStandings } from "~/models/qualifying-points"; import { getUpcomingScoringEvents, getRecentCompletedEvents, } from "~/models/scoring-event"; import { getRegularSeasonStandings } from "~/models/regular-season-standings"; import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value"; import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates"; import type { PlayoffMatch } from "~/models/playoff-match"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray } from "drizzle-orm"; export async function loader(args: Route.LoaderArgs) { const { userId } = await getAuth(args); const { params } = args; const { leagueId, sportsSeasonId } = params; // Check authentication if (!userId) { throw new Response("You must be logged in to view this page", { status: 401, }); } // Fetch league const league = await findLeagueById(leagueId); if (!league) { throw new Response("League not found", { status: 404 }); } // Check access: user must be a commissioner or member const isUserCommissioner = await isCommissioner(leagueId, userId); const isUserMember = await isUserLeagueMember(leagueId, userId); if (!isUserCommissioner && !isUserMember) { throw new Response("You do not have access to this league", { status: 403, }); } // Fetch sports season with relations const db = database(); const sportsSeason = await db.query.sportsSeasons.findFirst({ where: eq(schema.sportsSeasons.id, sportsSeasonId), with: { sport: true, seasonSports: { with: { season: true, }, }, }, }); if (!sportsSeason) { throw new Response("Sports season not found", { status: 404 }); } // Get the fantasy season for this league (to get teams and draft picks) // We need to find which season in this league includes this sports season // Filter by leagueId since a sports season can be linked to multiple leagues const seasonSport = sportsSeason.seasonSports?.find( (ss) => ss.season.leagueId === leagueId ); if (!seasonSport) { throw new Response("This sports season is not linked to this league", { status: 404, }); } const season = seasonSport.season; // Fetch teams and draft picks to determine ownership const teams = await findTeamsBySeasonId(season.id); const draftPicks = await getDraftPicks(season.id); // Build ownership map: participantId -> { teamName, teamId, ownerName } const ownershipMap = new Map< string, { teamName: string; teamId: string; ownerName?: string } >(); // Get unique owner IDs and batch-fetch their user records in a single query const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter(Boolean))] as string[]; const ownerUserRows = ownerIds.length > 0 ? await db.query.users.findMany({ where: inArray(schema.users.clerkId, ownerIds), columns: { clerkId: true, username: true, displayName: true }, }) : []; const ownerMap = new Map( ownerUserRows.map((u) => [u.clerkId, getUserDisplayName(u) ?? "Unknown"]) ); // Map draft picks to ownership for (const pick of draftPicks) { const team = teams.find((t) => t.id === pick.teamId); if (team && pick.participantId) { ownershipMap.set(pick.participantId, { teamName: team.name, teamId: team.id, ownerName: team.ownerId ? ownerMap.get(team.ownerId) : undefined, }); } } // Convert to array for serialization const teamOwnerships = Array.from(ownershipMap.entries()).map( ([participantId, data]) => ({ participantId, ...data, }) ); // Derive which participant IDs the current user has drafted const userTeams = teams.filter((t) => t.ownerId === userId); const userTeamIds = new Set(userTeams.map((t) => t.id)); const userParticipantIds = draftPicks .filter((p) => p.teamId && userTeamIds.has(p.teamId) && p.participantId) .map((p) => p.participantId as string); // Fetch pattern-specific data based on scoring pattern const scoringPattern = sportsSeason.scoringPattern; type SeasonStanding = { id: string; position: number; championshipPoints: string; participant: { id: string; name: string } }; type PlayoffMatchWithRelations = PlayoffMatch & { participant1: { id: string; name: string } | null; participant2: { id: string; name: string } | null; winner: { id: string; name: string } | null; loser: { id: string; name: string } | null; }; let playoffMatches: PlayoffMatchWithRelations[] = []; let playoffRounds: string[] = []; let preEliminatedParticipants: { id: string; name: string }[] = []; let participantPoints: { participantId: string; points: number }[] = []; let partialScoreParticipantIds: string[] = []; let seasonStandings: SeasonStanding[] = []; type QPStanding = Awaited>[number]; let qpStandings: QPStanding[] = []; if (scoringPattern === "playoff_bracket") { // Fetch playoff matches via scoring events const events = await db.query.scoringEvents.findMany({ where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId), }); let templateId: string | undefined; if (events.length > 0) { const eventIds = events.map((e) => e.id); const matches = await db.query.playoffMatches.findMany({ where: (pm) => inArray(pm.scoringEventId, eventIds), with: { participant1: true, participant2: true, winner: true, loser: true, }, }); playoffMatches = matches as PlayoffMatchWithRelations[]; templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined; const template = templateId ? getBracketTemplate(templateId) : undefined; playoffRounds = getOrderedRoundsFromMatches(matches, template); } // Fetch group-stage losers and all participant results for bracket scoring const [eliminatedResults, allResults] = await Promise.all([ db.query.participantResults.findMany({ where: and( eq(schema.participantResults.sportsSeasonId, sportsSeasonId), eq(schema.participantResults.finalPosition, 0) ), with: { participant: true }, }), db.query.participantResults.findMany({ where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId), }), ]); preEliminatedParticipants = eliminatedResults .filter((r): r is typeof r & { participant: NonNullable } => r.participant !== null && r.participant !== undefined) .map((r) => ({ id: r.participant.id, name: r.participant.name })); // Compute per-participant fantasy points directly from participant_results. // This covers both finalized placements and progressive floor scores for still-alive participants. const scoringRules = { pointsFor1st: season.pointsFor1st, pointsFor2nd: season.pointsFor2nd, pointsFor3rd: season.pointsFor3rd, pointsFor4th: season.pointsFor4th, pointsFor5th: season.pointsFor5th, pointsFor6th: season.pointsFor6th, pointsFor7th: season.pointsFor7th, pointsFor8th: season.pointsFor8th, }; const seenParticipantIds = new Set(); participantPoints = allResults .filter((r) => r.finalPosition !== null && r.finalPosition > 0) .filter((r) => { if (seenParticipantIds.has(r.participantId)) return false; seenParticipantIds.add(r.participantId); return true; }) .map((r) => ({ participantId: r.participantId, points: calculateBracketPoints(r.finalPosition ?? 0, scoringRules, templateId), })); // Track which participants have provisional (floor) scores — still competing partialScoreParticipantIds = allResults .filter((r) => r.isPartialScore) .map((r) => r.participantId); } else if (scoringPattern === "season_standings") { // Fetch F1-style championship standings and map to SeasonStanding shape const results = await getSeasonResults(sportsSeasonId); seasonStandings = results .filter((r) => r.currentPosition !== null || (r.currentPoints && parseFloat(r.currentPoints) > 0)) .map((r) => ({ id: r.id, position: r.currentPosition ?? 999, championshipPoints: r.currentPoints ?? "0", participant: { id: r.participant.id, name: r.participant.name, }, })); } else if (scoringPattern === "qualifying_points") { // Fetch qualifying points standings const standings = await getQPStandings(sportsSeasonId); qpStandings = standings; } // Fetch event schedule for all patterns (upcoming + recent) const [upcomingEvents, recentEvents] = await Promise.all([ getUpcomingScoringEvents(sportsSeasonId, 5), getRecentCompletedEvents(sportsSeasonId, 3), ]); // Fetch regular season standings for team-sport playoff_bracket seasons (NBA, NHL) const isTeamBracket = scoringPattern === "playoff_bracket" && sportsSeason.sport?.type === "team"; const [regularSeasonStandings, regularSeasonEvs] = isTeamBracket ? await Promise.all([ getRegularSeasonStandings(sportsSeasonId), getAllParticipantEVsForSeason(sportsSeasonId), ]) : [[], []]; // Derive seasonIsFinalized from status const seasonIsFinalized = sportsSeason.status === "completed"; // Build participantId → expectedValue map for display const participantEvs = Object.fromEntries( regularSeasonEvs.map((ev) => [ev.participantId, ev.expectedValue]) ); return { league, season, sportsSeason, scoringPattern, playoffMatches, playoffRounds, preEliminatedParticipants, participantPoints, partialScoreParticipantIds, seasonStandings, qpStandings, teamOwnerships, userParticipantIds, upcomingEvents, recentEvents, seasonIsFinalized, regularSeasonStandings, participantEvs, }; }