brackt/app/models/standings.ts

306 lines
8.8 KiB
TypeScript
Raw Normal View History

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, desc } from "drizzle-orm";
import type { TeamStanding, TeamStandingSnapshot } from "~/types/standings";
// Re-export types from shared types file
export type { TeamStanding, TeamStandingSnapshot } from "~/types/standings";
/**
* Get current standings for a season
*/
export async function getSeasonStandings(
seasonId: string,
providedDb?: ReturnType<typeof database>
): Promise<TeamStanding[]> {
const db = providedDb || database();
const standings = await db.query.teamStandings.findMany({
where: eq(schema.teamStandings.seasonId, seasonId),
with: {
team: true,
},
});
// Sort by currentRank ascending (1 is best, 2 is second, etc.)
const sorted = standings.sort((a, b) => a.currentRank - b.currentRank);
return sorted.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<typeof database>
): Promise<TeamStanding | null> {
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<typeof database>
) {
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<number, number> = {
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<string, typeof pickBreakdown> = {};
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<typeof database>
): Promise<TeamStandingSnapshot[]> {
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<typeof database>
) {
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<string, number>();
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<typeof database>
): Promise<void> {
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}`);
}