import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, desc } from "drizzle-orm"; export interface TeamStanding { teamId: string; teamName: string; totalPoints: number; currentRank: number; previousRank: number | null; rankChange: number; // Positive = moved up, negative = moved down placementCounts: { first: number; second: number; third: number; fourth: number; fifth: number; sixth: number; seventh: number; eighth: number; }; participantsRemaining: number; calculatedAt: Date; } export interface TeamStandingSnapshot { date: Date; rank: number; totalPoints: number; } /** * Get current standings for a season */ export async function getSeasonStandings( seasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const standings = await db.query.teamStandings.findMany({ where: eq(schema.teamStandings.seasonId, seasonId), orderBy: [ desc(schema.teamStandings.currentRank), // Lower rank number = better desc(schema.teamStandings.totalPoints), ], with: { team: true, }, }); return standings.map((standing) => ({ teamId: standing.teamId, teamName: standing.team.name, totalPoints: parseFloat(standing.totalPoints), currentRank: standing.currentRank, previousRank: standing.previousRank, rankChange: standing.previousRank ? standing.previousRank - standing.currentRank : 0, placementCounts: { first: standing.firstPlaceCount, second: standing.secondPlaceCount, third: standing.thirdPlaceCount, fourth: standing.fourthPlaceCount, fifth: standing.fifthPlaceCount, sixth: standing.sixthPlaceCount, seventh: standing.seventhPlaceCount, eighth: standing.eighthPlaceCount, }, participantsRemaining: standing.participantsRemaining, calculatedAt: standing.calculatedAt, })); } /** * Get standings for a specific team */ export async function getTeamStanding( teamId: string, seasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const standing = await db.query.teamStandings.findFirst({ where: and( eq(schema.teamStandings.teamId, teamId), eq(schema.teamStandings.seasonId, seasonId) ), with: { team: true, }, }); if (!standing) return null; return { teamId: standing.teamId, teamName: standing.team.name, totalPoints: parseFloat(standing.totalPoints), currentRank: standing.currentRank, previousRank: standing.previousRank, rankChange: standing.previousRank ? standing.previousRank - standing.currentRank : 0, placementCounts: { first: standing.firstPlaceCount, second: standing.secondPlaceCount, third: standing.thirdPlaceCount, fourth: standing.fourthPlaceCount, fifth: standing.fifthPlaceCount, sixth: standing.sixthPlaceCount, seventh: standing.seventhPlaceCount, eighth: standing.eighthPlaceCount, }, participantsRemaining: standing.participantsRemaining, calculatedAt: standing.calculatedAt, }; } /** * Get detailed team breakdown with all picks and their points */ export async function getTeamScoreBreakdown( teamId: string, seasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); // Get season scoring rules const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season) return null; // Get all draft picks for this team with participant details const picks = await db.query.draftPicks.findMany({ where: and( eq(schema.draftPicks.teamId, teamId), eq(schema.draftPicks.seasonId, seasonId) ), orderBy: schema.draftPicks.pickNumber, with: { participant: { with: { sportsSeason: { with: { sport: true, }, }, results: true, }, }, }, }); // Calculate points for each pick const pickBreakdown = picks.map((pick) => { const result = pick.participant.results[0]; let points = 0; if (result && result.finalPosition) { // Calculate points based on placement const pointsMap: Record = { 1: season.pointsFor1st, 2: season.pointsFor2nd, 3: season.pointsFor3rd, 4: season.pointsFor4th, 5: season.pointsFor5th, 6: season.pointsFor6th, 7: season.pointsFor7th, 8: season.pointsFor8th, }; points = pointsMap[result.finalPosition] || 0; } return { pickNumber: pick.pickNumber, round: pick.round, participant: { id: pick.participant.id, name: pick.participant.name, sport: pick.participant.sportsSeason.sport.name, }, finalPosition: result?.finalPosition || null, points, isComplete: !!result?.finalPosition, }; }); // Group by sport for easier display const bySport: Record = {}; for (const pick of pickBreakdown) { const sport = pick.participant.sport; if (!bySport[sport]) { bySport[sport] = []; } bySport[sport].push(pick); } return { team: await db.query.teams.findFirst({ where: eq(schema.teams.id, teamId), }), picks: pickBreakdown, bySport, totalPoints: pickBreakdown.reduce((sum, p) => sum + p.points, 0), completedCount: pickBreakdown.filter((p) => p.isComplete).length, totalCount: pickBreakdown.length, }; } /** * Get historical standings snapshots for a team * Used to display point progression over time */ export async function getTeamStandingsHistory( teamId: string, seasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const snapshots = await db.query.teamStandingsSnapshots.findMany({ where: and( eq(schema.teamStandingsSnapshots.teamId, teamId), eq(schema.teamStandingsSnapshots.seasonId, seasonId) ), orderBy: schema.teamStandingsSnapshots.snapshotDate, }); return snapshots.map((snapshot) => ({ date: new Date(snapshot.snapshotDate), rank: snapshot.rank, totalPoints: parseFloat(snapshot.totalPoints), })); } /** * Get standings comparison between current and 7 days ago * Used for "7-day change" display */ export async function getSevenDayStandingsChange( seasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); // Get current standings const current = await getSeasonStandings(seasonId, db); // Get snapshots from 7 days ago const snapshots = await db.query.teamStandingsSnapshots.findMany({ where: and( eq(schema.teamStandingsSnapshots.seasonId, seasonId), eq(schema.teamStandingsSnapshots.snapshotDate, sevenDaysAgo.toISOString().split("T")[0]) ), }); // Create a map of team -> old rank const oldRanks = new Map(); for (const snapshot of snapshots) { oldRanks.set(snapshot.teamId, snapshot.rank); } // Add 7-day changes to current standings return current.map((standing) => ({ ...standing, sevenDayRankChange: oldRanks.has(standing.teamId) ? oldRanks.get(standing.teamId)! - standing.currentRank : 0, sevenDayOldRank: oldRanks.get(standing.teamId) || null, })); } /** * Create a daily standings snapshot * Should be called by a scheduled job once per day */ export async function createDailySnapshot( seasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const today = new Date().toISOString().split("T")[0]; const standings = await db.query.teamStandings.findMany({ where: eq(schema.teamStandings.seasonId, seasonId), }); for (const standing of standings) { // Check if snapshot already exists for today const existing = await db.query.teamStandingsSnapshots.findFirst({ where: and( eq(schema.teamStandingsSnapshots.teamId, standing.teamId), eq(schema.teamStandingsSnapshots.seasonId, seasonId), eq(schema.teamStandingsSnapshots.snapshotDate, today) ), }); if (!existing) { await db.insert(schema.teamStandingsSnapshots).values({ teamId: standing.teamId, seasonId, snapshotDate: today, totalPoints: standing.totalPoints, rank: standing.currentRank, firstPlaceCount: standing.firstPlaceCount, secondPlaceCount: standing.secondPlaceCount, thirdPlaceCount: standing.thirdPlaceCount, fourthPlaceCount: standing.fourthPlaceCount, fifthPlaceCount: standing.fifthPlaceCount, sixthPlaceCount: standing.sixthPlaceCount, seventhPlaceCount: standing.seventhPlaceCount, eighthPlaceCount: standing.eighthPlaceCount, participantsRemaining: standing.participantsRemaining, }); } } console.log(`[Standings] Created daily snapshot for season ${seasonId}`); }