diff --git a/app/components/StandingsTable.tsx b/app/components/StandingsTable.tsx new file mode 100644 index 0000000..c15ebb5 --- /dev/null +++ b/app/components/StandingsTable.tsx @@ -0,0 +1,166 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { Badge } from "~/components/ui/badge"; +import { TrendingUp, TrendingDown, Minus, Trophy, Medal, Award } from "lucide-react"; + +export interface StandingsRow { + teamId: string; + teamName: string; + totalPoints: number; + currentRank: number; + previousRank?: number | null; + firstPlaceCount: number; + secondPlaceCount: number; + thirdPlaceCount: number; + fourthPlaceCount: number; + fifthPlaceCount: number; + sixthPlaceCount: number; + seventhPlaceCount: number; + eighthPlaceCount: number; + participantsRemaining: number; +} + +interface StandingsTableProps { + standings: StandingsRow[]; + showMovement?: boolean; + showPlacementBreakdown?: boolean; +} + +export function StandingsTable({ + standings, + showMovement = true, + showPlacementBreakdown = false, +}: StandingsTableProps) { + const getMovementIndicator = (current: number, previous?: number | null) => { + if (!showMovement || !previous) return null; + + const change = previous - current; // Positive means moved up + + if (change > 0) { + return ( +
+ + +{change} +
+ ); + } else if (change < 0) { + return ( +
+ + {change} +
+ ); + } else { + return ( +
+ + - +
+ ); + } + }; + + const getRankBadge = (rank: number) => { + if (rank === 1) { + return ( +
+ + {rank} +
+ ); + } else if (rank === 2) { + return ( +
+ + {rank} +
+ ); + } else if (rank === 3) { + return ( +
+ + {rank} +
+ ); + } else { + return {rank}; + } + }; + + return ( + + + + Rank + {showMovement && Change} + Team + Total Points + {showPlacementBreakdown && ( + <> + 🥇 + 🥈 + 🥉 + 4th + 5th + 6th + 7th + 8th + + )} + Remaining + + + + {standings.length === 0 ? ( + + + No standings data available yet + + + ) : ( + standings.map((row) => ( + + + {getRankBadge(row.currentRank)} + + {showMovement && ( + + {getMovementIndicator(row.currentRank, row.previousRank)} + + )} + {row.teamName} + + {row.totalPoints.toFixed(1)} + + {showPlacementBreakdown && ( + <> + {row.firstPlaceCount || "-"} + {row.secondPlaceCount || "-"} + {row.thirdPlaceCount || "-"} + {row.fourthPlaceCount || "-"} + {row.fifthPlaceCount || "-"} + {row.sixthPlaceCount || "-"} + {row.seventhPlaceCount || "-"} + {row.eighthPlaceCount || "-"} + + )} + + {row.participantsRemaining > 0 ? ( + {row.participantsRemaining} + ) : ( + - + )} + + + )) + )} + +
+ ); +} diff --git a/app/models/__tests__/scoring-calculator.test.ts b/app/models/__tests__/scoring-calculator.test.ts new file mode 100644 index 0000000..1178ed2 --- /dev/null +++ b/app/models/__tests__/scoring-calculator.test.ts @@ -0,0 +1,207 @@ +import { describe, it, expect } from "vitest"; +import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules"; + +const DEFAULT_SCORING: ScoringRules = { + pointsFor1st: 100, + pointsFor2nd: 70, + pointsFor3rd: 50, + pointsFor4th: 40, + pointsFor5th: 25, + pointsFor6th: 25, + pointsFor7th: 15, + pointsFor8th: 15, +}; + +describe("Scoring Calculator", () => { + describe("calculateFantasyPoints", () => { + it("should calculate points for 1st place", () => { + const points = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(points).toBe(100); + }); + + it("should calculate points for 2nd place", () => { + const points = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(points).toBe(70); + }); + + it("should calculate points for 8th place", () => { + const points = calculateFantasyPoints(8, DEFAULT_SCORING); + expect(points).toBe(15); + }); + + it("should return 0 for placements outside 1-8", () => { + const points = calculateFantasyPoints(9, DEFAULT_SCORING); + expect(points).toBe(0); + }); + + it("should handle custom scoring rules", () => { + const customScoring: ScoringRules = { + pointsFor1st: 200, + pointsFor2nd: 150, + pointsFor3rd: 100, + pointsFor4th: 75, + pointsFor5th: 50, + pointsFor6th: 40, + pointsFor7th: 30, + pointsFor8th: 20, + }; + + const points = calculateFantasyPoints(1, customScoring); + expect(points).toBe(200); + }); + }); + + describe("calculateAveragedPoints", () => { + it("should average points for two-way tie (SF losers sharing 3rd/4th)", () => { + const points = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + // (50 + 40) / 2 = 45 + expect(points).toBe(45); + }); + + it("should average points for four-way tie (QF losers sharing 5th-8th)", () => { + const points = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); + // (25 + 25 + 15 + 15) / 4 = 20 + expect(points).toBe(20); + }); + + it("should average points for three-way tie", () => { + const points = calculateAveragedPoints([1, 2, 3], DEFAULT_SCORING); + // (100 + 70 + 50) / 3 = 73.33... + expect(points).toBeCloseTo(73.33, 2); + }); + + it("should handle single placement (no tie)", () => { + const points = calculateAveragedPoints([1], DEFAULT_SCORING); + expect(points).toBe(100); + }); + + it("should return 0 for empty array", () => { + const points = calculateAveragedPoints([], DEFAULT_SCORING); + expect(points).toBe(0); + }); + + it("should work with custom scoring rules", () => { + const customScoring: ScoringRules = { + pointsFor1st: 80, + pointsFor2nd: 60, + pointsFor3rd: 40, + pointsFor4th: 30, + pointsFor5th: 20, + pointsFor6th: 20, + pointsFor7th: 10, + pointsFor8th: 10, + }; + + const points = calculateAveragedPoints([5, 6, 7, 8], customScoring); + // (20 + 20 + 10 + 10) / 4 = 15 + expect(points).toBe(15); + }); + }); + + describe("Playoff Placement Scenarios", () => { + it("should handle 8-team single elimination bracket", () => { + // Champion: 1st (100 pts) + const champion = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(champion).toBe(100); + + // Runner-up: 2nd (70 pts) + const runnerUp = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(runnerUp).toBe(70); + + // SF losers: share 3rd/4th (45 pts each) + const sfLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(sfLosers).toBe(45); + + // QF losers: share 5th-8th (20 pts each) + const qfLosers = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); + expect(qfLosers).toBe(20); + }); + + it("should handle 4-team bracket (no quarterfinals)", () => { + // Champion: 1st (100 pts) + const champion = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(champion).toBe(100); + + // Runner-up: 2nd (70 pts) + const runnerUp = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(runnerUp).toBe(70); + + // SF losers: share 3rd/4th (45 pts each) + const sfLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(sfLosers).toBe(45); + }); + }); + + describe("Edge Cases", () => { + it("should handle all participants tying for 1st-8th", () => { + const points = calculateAveragedPoints([1, 2, 3, 4, 5, 6, 7, 8], DEFAULT_SCORING); + // (100 + 70 + 50 + 40 + 25 + 25 + 15 + 15) / 8 = 42.5 + expect(points).toBe(42.5); + }); + + it("should handle placements with same point values", () => { + // With default scoring, 5th and 6th both get 25 points + const points56 = calculateAveragedPoints([5, 6], DEFAULT_SCORING); + expect(points56).toBe(25); + + // 7th and 8th both get 15 points + const points78 = calculateAveragedPoints([7, 8], DEFAULT_SCORING); + expect(points78).toBe(15); + }); + + it("should handle zero points for eliminated participants", () => { + // According to Q20, participants eliminated before Elite Eight get 0 points + const points = calculateFantasyPoints(9, DEFAULT_SCORING); + expect(points).toBe(0); + + const points10 = calculateFantasyPoints(16, DEFAULT_SCORING); + expect(points10).toBe(0); + }); + }); + + describe("Real-world Scenarios", () => { + it("should correctly calculate NFL playoff points", () => { + // NFL has 8 teams in playoffs (QF, SF, Finals) + // Super Bowl winner: 100 pts + const winner = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(winner).toBe(100); + + // Super Bowl loser: 70 pts + const loser = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(loser).toBe(70); + + // Conference championship losers: 45 pts each + const confLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(confLosers).toBe(45); + + // Divisional round losers: 20 pts each + const divLosers = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); + expect(divLosers).toBe(20); + }); + + it("should correctly calculate NCAA March Madness Elite Eight points", () => { + // Even though 68 teams start, only Elite Eight (8 teams) get points + // Teams eliminated before Elite Eight get 0 points + + // Champion: 100 pts + const champion = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(champion).toBe(100); + + // Runner-up: 70 pts + const runnerUp = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(runnerUp).toBe(70); + + // Final Four losers: 45 pts each + const ffLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(ffLosers).toBe(45); + + // Elite Eight losers: 20 pts each + const e8Losers = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); + expect(e8Losers).toBe(20); + + // Sweet Sixteen loser (example): 0 pts + const sweetSixteenLoser = calculateFantasyPoints(9, DEFAULT_SCORING); + expect(sweetSixteenLoser).toBe(0); + }); + }); +}); diff --git a/app/models/participant-result.ts b/app/models/participant-result.ts index 1511647..a893664 100644 --- a/app/models/participant-result.ts +++ b/app/models/participant-result.ts @@ -101,20 +101,8 @@ export async function deleteParticipantResultsBySportsSeasonId( } /** - * Calculate points awarded based on final position - * 1st: 80, 2nd: 50, 3rd-4th: 30, 5th-8th: 20, 9th+: 0 - */ -export function calculatePointsFromPosition(position: number | null): number { - if (position === null || position < 1) return 0; - if (position === 1) return 80; - if (position === 2) return 50; - if (position >= 3 && position <= 4) return 30; - if (position >= 5 && position <= 8) return 20; - return 0; -} - -/** - * Set result with automatic point calculation + * Set result for a participant in a sports season + * Points are calculated on-demand based on each fantasy league's scoring rules */ export async function setParticipantResult( participantId: string, @@ -124,7 +112,6 @@ export async function setParticipantResult( notes?: string ): Promise { const db = database(); - const pointsAwarded = calculatePointsFromPosition(finalPosition); // Check if result already exists const existing = await db.query.participantResults.findFirst({ @@ -138,7 +125,6 @@ export async function setParticipantResult( // Update existing result return await updateParticipantResult(existing.id, { finalPosition, - pointsAwarded, qualifyingPoints: qualifyingPoints?.toString(), notes, }); @@ -148,7 +134,6 @@ export async function setParticipantResult( participantId, sportsSeasonId, finalPosition, - pointsAwarded, qualifyingPoints: qualifyingPoints?.toString(), notes, }); diff --git a/app/models/playoff-match.ts b/app/models/playoff-match.ts new file mode 100644 index 0000000..6828d84 --- /dev/null +++ b/app/models/playoff-match.ts @@ -0,0 +1,251 @@ +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; + +export type PlayoffMatch = typeof schema.playoffMatches.$inferSelect; +export type NewPlayoffMatch = typeof schema.playoffMatches.$inferInsert; + +export async function createPlayoffMatch( + data: NewPlayoffMatch +): Promise { + const db = database(); + const [match] = await db + .insert(schema.playoffMatches) + .values(data) + .returning(); + return match; +} + +export async function createManyPlayoffMatches( + data: NewPlayoffMatch[] +): Promise { + const db = database(); + return await db + .insert(schema.playoffMatches) + .values(data) + .returning(); +} + +export async function findPlayoffMatchById( + id: string +): Promise { + const db = database(); + return await db.query.playoffMatches.findFirst({ + where: eq(schema.playoffMatches.id, id), + with: { + scoringEvent: true, + participant1: true, + participant2: true, + winner: true, + loser: true, + }, + }); +} + +export async function findPlayoffMatchesByEventId( + eventId: string +): Promise { + const db = database(); + return await db.query.playoffMatches.findMany({ + where: eq(schema.playoffMatches.scoringEventId, eventId), + orderBy: (matches, { asc }) => [asc(matches.matchNumber)], + with: { + participant1: true, + participant2: true, + winner: true, + loser: true, + }, + }); +} + +export async function findPlayoffMatchesByEventIdAndRound( + eventId: string, + round: string +): Promise { + const db = database(); + return await db.query.playoffMatches.findMany({ + where: and( + eq(schema.playoffMatches.scoringEventId, eventId), + eq(schema.playoffMatches.round, round) + ), + orderBy: (matches, { asc }) => [asc(matches.matchNumber)], + with: { + participant1: true, + participant2: true, + winner: true, + loser: true, + }, + }); +} + +export async function updatePlayoffMatch( + id: string, + data: Partial +): Promise { + const db = database(); + const [match] = await db + .update(schema.playoffMatches) + .set({ ...data, updatedAt: new Date() }) + .where(eq(schema.playoffMatches.id, id)) + .returning(); + return match; +} + +export async function deletePlayoffMatch(id: string): Promise { + const db = database(); + await db.delete(schema.playoffMatches).where(eq(schema.playoffMatches.id, id)); +} + +export async function deletePlayoffMatchesByEventId( + eventId: string +): Promise { + const db = database(); + await db + .delete(schema.playoffMatches) + .where(eq(schema.playoffMatches.scoringEventId, eventId)); +} + +/** + * Set the winner of a playoff match + */ +export async function setMatchWinner( + matchId: string, + winnerId: string, + loserId: string, + participant1Score?: number, + participant2Score?: number +): Promise { + const db = database(); + const [match] = await db + .update(schema.playoffMatches) + .set({ + winnerId, + loserId, + isComplete: true, + participant1Score: participant1Score?.toString(), + participant2Score: participant2Score?.toString(), + updatedAt: new Date(), + }) + .where(eq(schema.playoffMatches.id, matchId)) + .returning(); + return match; +} + +/** + * Generate a standard single elimination bracket structure + */ +export async function generateSingleEliminationBracket( + eventId: string, + participantIds: string[] +): Promise { + const db = database(); + const numParticipants = participantIds.length; + + // Validate that we have 4 or 8 participants for standard bracket + if (numParticipants !== 4 && numParticipants !== 8) { + throw new Error("Single elimination bracket requires 4 or 8 participants"); + } + + const matches: NewPlayoffMatch[] = []; + + if (numParticipants === 8) { + // Quarterfinals (4 matches) + for (let i = 0; i < 4; i++) { + matches.push({ + scoringEventId: eventId, + round: "Quarterfinals", + matchNumber: i + 1, + participant1Id: participantIds[i * 2], + participant2Id: participantIds[i * 2 + 1], + isComplete: false, + }); + } + + // Semifinals (2 matches) + for (let i = 0; i < 2; i++) { + matches.push({ + scoringEventId: eventId, + round: "Semifinals", + matchNumber: i + 1, + participant1Id: null, + participant2Id: null, + isComplete: false, + }); + } + } else if (numParticipants === 4) { + // Semifinals (2 matches) + for (let i = 0; i < 2; i++) { + matches.push({ + scoringEventId: eventId, + round: "Semifinals", + matchNumber: i + 1, + participant1Id: participantIds[i * 2], + participant2Id: participantIds[i * 2 + 1], + isComplete: false, + }); + } + } + + // Finals (1 match) + matches.push({ + scoringEventId: eventId, + round: "Finals", + matchNumber: 1, + participant1Id: null, + participant2Id: null, + isComplete: false, + }); + + return await createManyPlayoffMatches(matches); +} + +/** + * Advance the winner of a match to the next round + * Uses deterministic slot assignment to maintain bracket structure + */ +export async function advanceWinner( + matchId: string, + winnerId: string +): Promise { + const db = database(); + + // Get the match + const match = await findPlayoffMatchById(matchId); + if (!match) throw new Error("Match not found"); + + // Determine which round this is and where to advance + const eventId = match.scoringEventId; + let nextRound: string; + let nextMatchNumber: number; + let participantSlot: 'participant1Id' | 'participant2Id'; + + if (match.round === "Quarterfinals") { + nextRound = "Semifinals"; + // Match 1,2 -> SF1, Match 3,4 -> SF2 + nextMatchNumber = match.matchNumber <= 2 ? 1 : 2; + // Odd matches (1, 3) -> participant1, Even matches (2, 4) -> participant2 + participantSlot = match.matchNumber % 2 === 1 ? 'participant1Id' : 'participant2Id'; + } else if (match.round === "Semifinals") { + nextRound = "Finals"; + nextMatchNumber = 1; + // SF Match 1 -> Finals participant1, SF Match 2 -> Finals participant2 + participantSlot = match.matchNumber === 1 ? 'participant1Id' : 'participant2Id'; + } else { + // Finals - no advancement needed + return; + } + + // Find the next match + const nextMatches = await findPlayoffMatchesByEventIdAndRound(eventId, nextRound); + const nextMatch = nextMatches.find(m => m.matchNumber === nextMatchNumber); + + if (!nextMatch) throw new Error("Next match not found"); + + // Check if the slot is already filled + if (nextMatch[participantSlot]) { + throw new Error(`Next match ${participantSlot} is already filled`); + } + + // Fill the determined slot + await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId }); +} diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index f58fe44..5b4e649 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -16,7 +16,11 @@ export type ScoringPattern = /** * Process a playoff event completion and assign final placements - * Implementation will be added in Phase 2 + * + * Q19: Admin enters the same placement for all tied participants, + * system automatically shares positions + * + * Q20: Participants eliminated before Elite Eight get 0 points */ export async function processPlayoffEvent( eventId: string, @@ -24,14 +28,134 @@ export async function processPlayoffEvent( ): Promise { const db = providedDb || database(); - // TODO: Implement in Phase 2 - // 1. Get event and determine round (QF, SF, Finals) - // 2. Get matches and determine winners/losers - // 3. Assign shared placements based on elimination round - // 4. Update participantResults with final placements - // 5. Trigger recalculation for affected leagues + // Get the event + const event = await db.query.scoringEvents.findFirst({ + where: eq(schema.scoringEvents.id, eventId), + with: { + sportsSeason: { + with: { + seasonSports: { + with: { + season: true, + }, + }, + }, + }, + }, + }); - console.log(`[ScoringCalculator] processPlayoffEvent not yet implemented: ${eventId}`); + if (!event) { + throw new Error(`Event ${eventId} not found`); + } + + if (!event.playoffRound) { + throw new Error(`Event ${eventId} is not a playoff event`); + } + + // Get all matches for this event + const matches = await db.query.playoffMatches.findMany({ + where: eq(schema.playoffMatches.scoringEventId, eventId), + orderBy: (matches, { asc }) => [asc(matches.matchNumber)], + }); + + // Process based on round + const round = event.playoffRound; + + // Get season scoring rules for the first affected season + // (all seasons should have their own rules, but we use the event's sports season to find one) + const seasonSport = event.sportsSeason.seasonSports[0]; + if (!seasonSport) { + throw new Error(`No fantasy seasons found for sports season ${event.sportsSeasonId}`); + } + + const scoringRules = await getScoringRules(seasonSport.seasonId, db); + if (!scoringRules) { + throw new Error(`Scoring rules not found for season ${seasonSport.seasonId}`); + } + + if (round === "Finals") { + // Winner gets 1st, loser gets 2nd + const finalMatch = matches[0]; + if (!finalMatch || !finalMatch.winnerId || !finalMatch.loserId) { + throw new Error("Finals match is not complete"); + } + + // Update or create participant results for winner + await upsertParticipantResult( + finalMatch.winnerId, + event.sportsSeasonId, + 1, + db + ); + + // Update or create participant results for loser + await upsertParticipantResult( + finalMatch.loserId, + event.sportsSeasonId, + 2, + db + ); + } else if (round === "Semifinals") { + // Losers share 3rd/4th + for (const match of matches) { + if (match.loserId) { + await upsertParticipantResult( + match.loserId, + event.sportsSeasonId, + 3, // Store as placement 3 (the first shared placement) + db + ); + } + } + } else if (round === "Quarterfinals") { + // Losers share 5th-8th + for (const match of matches) { + if (match.loserId) { + await upsertParticipantResult( + match.loserId, + event.sportsSeasonId, + 5, // Store as placement 5 (the first shared placement) + db + ); + } + } + } + + // Recalculate standings for all affected leagues + await recalculateAffectedLeagues(event.sportsSeasonId, db); +} + +/** + * Helper to upsert participant result + */ +async function upsertParticipantResult( + participantId: string, + sportsSeasonId: string, + finalPosition: number, + db: ReturnType +): Promise { + const existing = await db.query.participantResults.findFirst({ + where: and( + eq(schema.participantResults.participantId, participantId), + eq(schema.participantResults.sportsSeasonId, sportsSeasonId) + ), + }); + + if (existing) { + await db + .update(schema.participantResults) + .set({ + finalPosition, + updatedAt: new Date(), + }) + .where(eq(schema.participantResults.id, existing.id)); + } else { + await db.insert(schema.participantResults).values({ + participantId, + sportsSeasonId, + finalPosition, + }); + } } /** diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index d1d526a..f4da2fe 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -2,7 +2,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, desc } from "drizzle-orm"; -export type EventType = "playoff_game" | "major_tournament" | "race" | "final_standings"; +export type EventType = "playoff_game" | "major_tournament" | "final_standings"; export interface CreateScoringEventData { sportsSeasonId: string; @@ -16,6 +16,7 @@ export interface CreateScoringEventData { export interface UpdateScoringEventData { name?: string; eventDate?: Date; + playoffRound?: string; isComplete?: boolean; completedAt?: Date; } diff --git a/app/routes.ts b/app/routes.ts index 108a74a..7afc19c 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -52,6 +52,10 @@ export default [ "sports-seasons/:id/events/:eventId", "routes/admin.sports-seasons.$id.events.$eventId.tsx" ), + route( + "sports-seasons/:id/events/:eventId/bracket", + "routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx" + ), route("participants", "routes/admin.participants.tsx"), route("templates", "routes/admin.templates.tsx"), route("templates/new", "routes/admin.templates.new.tsx"), diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts new file mode 100644 index 0000000..2c23fe7 --- /dev/null +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -0,0 +1,255 @@ +import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; +import { redirect } from "react-router"; +import { findSportsSeasonById } from "~/models/sports-season"; +import { findParticipantsBySportsSeasonId } from "~/models/participant"; +import { getScoringEventById } from "~/models/scoring-event"; +import { + findPlayoffMatchesByEventId, + generateSingleEliminationBracket, + setMatchWinner, + advanceWinner, + findPlayoffMatchById, +} from "~/models/playoff-match"; +import { processPlayoffEvent } from "~/models/scoring-calculator"; + +export async function loader({ params }: Route.LoaderArgs) { + const sportsSeason = await findSportsSeasonById(params.id); + + if (!sportsSeason) { + throw new Response("Sports season not found", { status: 404 }); + } + + const event = await getScoringEventById(params.eventId); + + if (!event) { + throw new Response("Event not found", { status: 404 }); + } + + if (event.eventType !== "playoff_game") { + throw new Response("This event is not a playoff event", { status: 400 }); + } + + const participants = await findParticipantsBySportsSeasonId(params.id); + const matches = await findPlayoffMatchesByEventId(params.eventId); + + // The matches already include participant relations from the model query + return { + sportsSeason: sportsSeason as typeof sportsSeason & { + sport: { id: string; name: string; type: string; slug: string }; + }, + event, + participants, + matches: matches as Array, + }; +} + +export async function action({ request, params }: Route.ActionArgs) { + const formData = await request.formData(); + const intent = formData.get("intent"); + + if (intent === "generate-bracket") { + const bracketSize = formData.get("bracketSize"); + + if (bracketSize !== "4" && bracketSize !== "8") { + return { error: "Invalid bracket size" }; + } + + const size = parseInt(bracketSize, 10); + const participantIds: string[] = []; + + for (let i = 0; i < size; i++) { + const participantId = formData.get(`participant${i}`); + if (typeof participantId !== "string" || !participantId) { + return { error: `Participant ${i + 1} is required` }; + } + participantIds.push(participantId); + } + + // Check for duplicates + const uniqueParticipants = new Set(participantIds); + if (uniqueParticipants.size !== participantIds.length) { + return { error: "Each participant can only be selected once" }; + } + + try { + await generateSingleEliminationBracket(params.eventId, participantIds); + return { success: "Bracket generated successfully" }; + } catch (error) { + console.error("Error generating bracket:", error); + return { + error: + error instanceof Error + ? error.message + : "Failed to generate bracket", + }; + } + } + + if (intent === "set-winner") { + const matchId = formData.get("matchId"); + const winnerId = formData.get("winnerId"); + + if (typeof matchId !== "string" || !matchId) { + return { error: "Match ID is required" }; + } + + if (typeof winnerId !== "string" || !winnerId) { + return { error: "Winner ID is required" }; + } + + try { + const match = await findPlayoffMatchById(matchId); + if (!match) { + return { error: "Match not found" }; + } + + // Determine loser + const loserId = + match.participant1Id === winnerId + ? match.participant2Id + : match.participant1Id; + + if (!loserId) { + return { error: "Could not determine loser" }; + } + + // Set the winner + await setMatchWinner(matchId, winnerId, loserId); + + // Try to advance the winner to the next round (if not Finals) + if (match.round !== "Finals") { + try { + await advanceWinner(matchId, winnerId); + } catch (error) { + // Only ignore "already filled" errors, re-throw unexpected errors + if ( + error instanceof Error && + error.message.includes("already filled") + ) { + console.warn("Match already filled, skipping advancement"); + } else { + throw error; // Re-throw unexpected errors + } + } + } + + return { success: "Winner set successfully" }; + } catch (error) { + console.error("Error setting winner:", error); + return { + error: + error instanceof Error ? error.message : "Failed to set winner", + }; + } + } + + if (intent === "complete-round") { + const round = formData.get("round"); + + if ( + round !== "Quarterfinals" && + round !== "Semifinals" && + round !== "Finals" + ) { + return { error: "Invalid round" }; + } + + try { + // Get the event and update playoffRound to the completed round + const event = await getScoringEventById(params.eventId); + if (!event) { + return { error: "Event not found" }; + } + + // Verify all matches in this round are complete + const matches = await findPlayoffMatchesByEventId(params.eventId); + const roundMatches = matches.filter((m) => m.round === round); + + const allComplete = roundMatches.every((m) => m.isComplete); + if (!allComplete) { + return { error: `Not all matches in ${round} are complete` }; + } + + // Validate round order: ensure previous rounds are complete + const { findParticipantResultsBySportsSeasonId } = await import( + "~/models/participant-result" + ); + const existingResults = await findParticipantResultsBySportsSeasonId( + params.id + ); + + if (round === "Finals") { + // Finals requires Semifinals to be complete (3rd/4th place results exist) + const hasSemifinalResults = existingResults.some( + (r) => r.finalPosition === 3 || r.finalPosition === 4 + ); + if (!hasSemifinalResults) { + return { + error: + "Semifinals must be completed before Finals (no 3rd/4th place results found)", + }; + } + } else if (round === "Semifinals") { + // Semifinals requires Quarterfinals to be complete if QF matches exist + const hasQuarterfinals = matches.some((m) => m.round === "Quarterfinals"); + if (hasQuarterfinals) { + const hasQuarterfinalsResults = existingResults.some( + (r) => + r.finalPosition === 5 || + r.finalPosition === 6 || + r.finalPosition === 7 || + r.finalPosition === 8 + ); + if (!hasQuarterfinalsResults) { + return { + error: + "Quarterfinals must be completed before Semifinals (no 5th-8th place results found)", + }; + } + } + } + + // Get the sports season to find a fantasy season for scoring rules + const sportsSeason = await findSportsSeasonById(params.id); + if (!sportsSeason) { + return { error: "Sports season not found" }; + } + + // Process the playoff event to calculate placements + // We need to temporarily update the event's playoffRound to the one being completed + const { updateScoringEvent } = await import("~/models/scoring-event"); + await updateScoringEvent(params.eventId, { playoffRound: round }); + + // Get a fantasy season ID for scoring calculation + const { findSeasonSportsBySportsSeasonId } = await import( + "~/models/season-sport" + ); + const seasonSports = await findSeasonSportsBySportsSeasonId(params.id); + if (seasonSports.length === 0) { + return { + error: "No fantasy seasons found for this sports season", + }; + } + + // Process playoff event to update participant results + await processPlayoffEvent(params.eventId); + + return { + success: `${round} completed and placements calculated successfully`, + }; + } catch (error) { + console.error("Error completing round:", error); + return { + error: + error instanceof Error ? error.message : "Failed to complete round", + }; + } + } + + return { error: "Invalid action" }; +} diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx new file mode 100644 index 0000000..4e94392 --- /dev/null +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx @@ -0,0 +1,278 @@ +import { Form, Link } from "react-router"; +import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; +import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.server"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { ArrowLeft, Plus, Trophy } from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; + +export { loader, action }; + +export default function EventBracket({ + loaderData, + actionData, +}: Route.ComponentProps) { + const { sportsSeason, event, participants, matches } = loaderData; + + // Group matches by round + const matchesByRound: Record = {}; + for (const match of matches) { + if (!matchesByRound[match.round]) { + matchesByRound[match.round] = []; + } + matchesByRound[match.round].push(match); + } + + const rounds = ["Quarterfinals", "Semifinals", "Finals"]; + const availableRounds = rounds.filter(r => matchesByRound[r]); + + // Get participants not yet in any match + const participantsInMatches = new Set( + matches.flatMap(m => [m.participant1Id, m.participant2Id].filter(Boolean)) + ); + const availableParticipants = participants.filter( + (p: { id: string }) => !participantsInMatches.has(p.id) + ); + + return ( +
+
+
+ +
+
+

Playoff Bracket

+

+ {event.name} - {sportsSeason.sport.name} {sportsSeason.name} +

+
+
+
+ +
+ {actionData?.error && ( +
+ {actionData.error} +
+ )} + + {actionData?.success && ( +
+ {actionData.success} +
+ )} + + {/* Generate Bracket Form */} + {matches.length === 0 && ( + + + Generate Bracket + + Create the bracket structure for this playoff event + + + +
+ + +
+ + +
+ +
+ +

+ Select {8} participants for the bracket +

+ {[...Array(8)].map((_, i) => ( + + ))} +
+ + +
+
+
+ )} + + {/* Bracket Display */} + {availableRounds.map(round => ( + + + {round} + + {matchesByRound[round].length} {matchesByRound[round].length === 1 ? "match" : "matches"} + + + + + + + Match + Participant 1 + Score + vs + Score + Participant 2 + Winner + Actions + + + + {matchesByRound[round].map((match) => ( + + + {match.matchNumber} + + + {match.participant1?.name || "TBD"} + + + {match.participant1Score ? parseFloat(match.participant1Score) : "-"} + + + vs + + + {match.participant2Score ? parseFloat(match.participant2Score) : "-"} + + + {match.participant2?.name || "TBD"} + + + {match.winner?.name ? ( +
+ + {match.winner.name} +
+ ) : ( + "-" + )} +
+ + {!match.isComplete && match.participant1Id && match.participant2Id ? ( +
+ + + + + + ) : match.isComplete ? ( + Complete + ) : ( + Waiting + )} +
+
+ ))} +
+
+
+
+ ))} + + {/* Complete Round Button */} + {availableRounds.length > 0 && ( + + + Complete Round + + Finalize the current round and calculate placements + + + +
+ +
+
+ + +
+ +
+
+
+
+ )} +
+
+
+ ); +} diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx index 88f5f6e..d59e294 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -19,7 +19,7 @@ import { SelectValue, } from "~/components/ui/select"; import { Badge } from "~/components/ui/badge"; -import { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2 } from "lucide-react"; +import { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2, Brackets } from "lucide-react"; import { Table, TableBody, @@ -53,11 +53,9 @@ export default function EventResults({ const getEventTypeLabel = (type: string) => { switch (type) { case "playoff_game": - return "Playoff Game"; + return "Bracket"; case "major_tournament": return "Major Tournament"; - case "race": - return "Race"; case "final_standings": return "Final Standings"; default: @@ -84,14 +82,24 @@ export default function EventResults({ {event.playoffRound && ` • ${event.playoffRound}`}

- {event.isComplete ? ( - - - Completed - - ) : ( - In Progress - )} +
+ {event.eventType === "playoff_game" && ( + + )} + {event.isComplete ? ( + + + Completed + + ) : ( + In Progress + )} +
diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index c1dd071..5589b5b 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -58,7 +58,6 @@ export async function action({ request, params }: Route.ActionArgs) { if ( eventType !== "playoff_game" && eventType !== "major_tournament" && - eventType !== "race" && eventType !== "final_standings" ) { return { error: "Invalid event type" }; @@ -67,7 +66,7 @@ export async function action({ request, params }: Route.ActionArgs) { const eventData: CreateScoringEventData = { sportsSeasonId: params.id, name: name.trim(), - eventType: eventType as "playoff_game" | "major_tournament" | "race" | "final_standings", + eventType: eventType as "playoff_game" | "major_tournament" | "final_standings", eventDate: typeof eventDate === "string" && eventDate ? new Date(eventDate) : undefined, playoffRound: typeof playoffRound === "string" && playoffRound ? playoffRound : undefined, }; diff --git a/app/routes/admin.sports-seasons.$id.events.tsx b/app/routes/admin.sports-seasons.$id.events.tsx index b4207fd..1804334 100644 --- a/app/routes/admin.sports-seasons.$id.events.tsx +++ b/app/routes/admin.sports-seasons.$id.events.tsx @@ -33,11 +33,9 @@ export default function SportsSeasonEvents({ const getEventTypeLabel = (type: string) => { switch (type) { case "playoff_game": - return "Playoff Game"; + return "Bracket"; case "major_tournament": return "Major Tournament"; - case "race": - return "Race"; case "final_standings": return "Final Standings"; default: @@ -77,7 +75,7 @@ export default function SportsSeasonEvents({ Create New Event - Add a new scoring event (game, tournament, race, etc.) + Add a new scoring event (bracket, tournament, standings, etc.) @@ -101,9 +99,8 @@ export default function SportsSeasonEvents({ - Playoff Game + Bracket Major Tournament - Race Final Standings diff --git a/app/routes/admin.sports-seasons.$id.tsx b/app/routes/admin.sports-seasons.$id.tsx index 6e3d0c6..d6aed42 100644 --- a/app/routes/admin.sports-seasons.$id.tsx +++ b/app/routes/admin.sports-seasons.$id.tsx @@ -30,7 +30,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; -import { Trash2, Users } from "lucide-react"; +import { Trash2, Users, Trophy } from "lucide-react"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -266,6 +266,31 @@ export default function EditSportsSeason({ loaderData, actionData }: Route.Compo + + +
+
+ Scoring Events + + Manage games, tournaments, and results + +
+ +
+
+ +

+ Create playoff games, tournaments, races, or standings events to track participant results and calculate fantasy points. +

+
+
+ Danger Zone diff --git a/database/schema.ts b/database/schema.ts index e87328b..dc28541 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -49,7 +49,6 @@ export const scoringPatternEnum = pgEnum("scoring_pattern", [ export const eventTypeEnum = pgEnum("event_type", [ "playoff_game", "major_tournament", - "race", "final_standings", ]); @@ -288,7 +287,6 @@ export const participantResults = pgTable("participant_results", { .notNull() .references(() => sportsSeasons.id, { onDelete: "cascade" }), finalPosition: integer("final_position"), - pointsAwarded: integer("points_awarded").notNull().default(0), qualifyingPoints: decimal("qualifying_points", { precision: 10, scale: 2 }), notes: text("notes"), createdAt: timestamp("created_at").defaultNow().notNull(), diff --git a/drizzle/0021_fair_ser_duncan.sql b/drizzle/0021_fair_ser_duncan.sql new file mode 100644 index 0000000..f8e665a --- /dev/null +++ b/drizzle/0021_fair_ser_duncan.sql @@ -0,0 +1,4 @@ +ALTER TABLE "public"."scoring_events" ALTER COLUMN "event_type" SET DATA TYPE text;--> statement-breakpoint +DROP TYPE "public"."event_type";--> statement-breakpoint +CREATE TYPE "public"."event_type" AS ENUM('playoff_game', 'major_tournament', 'final_standings');--> statement-breakpoint +ALTER TABLE "public"."scoring_events" ALTER COLUMN "event_type" SET DATA TYPE "public"."event_type" USING "event_type"::"public"."event_type"; \ No newline at end of file diff --git a/drizzle/0022_shiny_mother_askani.sql b/drizzle/0022_shiny_mother_askani.sql new file mode 100644 index 0000000..9a5697f --- /dev/null +++ b/drizzle/0022_shiny_mother_askani.sql @@ -0,0 +1 @@ +ALTER TABLE "participant_results" DROP COLUMN IF EXISTS "points_awarded"; \ No newline at end of file diff --git a/drizzle/meta/0021_snapshot.json b/drizzle/meta/0021_snapshot.json new file mode 100644 index 0000000..3ea68d8 --- /dev/null +++ b/drizzle/meta/0021_snapshot.json @@ -0,0 +1,2706 @@ +{ + "id": "122d89a4-2d5c-4a52-8aa9-aa507315831c", + "prevId": "5d2991a0-ecb3-488a-89d1-cf5e36b4d7f5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_participants_id_fk": { + "name": "draft_picks_participant_id_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_participants_id_fk": { + "name": "draft_queue_participant_id_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_participant_id_participants_id_fk": { + "name": "event_results_participant_id_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_expected_values": { + "name": "participant_expected_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_expected_values_participant_id_participants_id_fk": { + "name": "participant_expected_values_participant_id_participants_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_expected_values_season_id_seasons_id_fk": { + "name": "participant_expected_values_season_id_seasons_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_qualifying_totals": { + "name": "participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_qualifying_totals_participant_id_participants_id_fk": { + "name": "participant_qualifying_totals_participant_id_participants_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_results": { + "name": "participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points_awarded": { + "name": "points_awarded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_results_participant_id_participants_id_fk": { + "name": "participant_results_participant_id_participants_id_fk", + "tableFrom": "participant_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_season_results_participant_id_participants_id_fk": { + "name": "participant_season_results_participant_id_participants_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_season_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_season_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participants": { + "name": "participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participants_sports_season_id_sports_seasons_id_fk": { + "name": "participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_matches": { + "name": "playoff_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "match_number": { + "name": "match_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "loser_id": { + "name": "loser_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_matches_scoring_event_id_scoring_events_id_fk": { + "name": "playoff_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_matches_participant1_id_participants_id_fk": { + "name": "playoff_matches_participant1_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_participant2_id_participants_id_fk": { + "name": "playoff_matches_participant2_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_winner_id_participants_id_fk": { + "name": "playoff_matches_winner_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_loser_id_participants_id_fk": { + "name": "playoff_matches_loser_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "loser_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifying_point_config": { + "name": "qualifying_point_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "qualifying_point_config_sports_season_id_sports_seasons_id_fk": { + "name": "qualifying_point_config_sports_season_id_sports_seasons_id_fk", + "tableFrom": "qualifying_point_config", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scoring_events_sports_season_id_sports_seasons_id_fk": { + "name": "scoring_events_sports_season_id_sports_seasons_id_fk", + "tableFrom": "scoring_events", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_template_sports_template_id_season_templates_id_fk": { + "name": "season_template_sports_template_id_season_templates_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_template_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_template_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_templates": { + "name": "season_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "draft_rounds": { + "name": "draft_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "flex_spots": { + "name": "flex_spots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draft_date_time": { + "name": "draft_date_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "scoring_type": { + "name": "scoring_type", + "type": "scoring_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "scoring_pattern": { + "name": "scoring_pattern", + "type": "scoring_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_majors": { + "name": "total_majors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "majors_completed": { + "name": "majors_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points_finalized": { + "name": "qualifying_points_finalized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sports_seasons_sport_id_sports_id_fk": { + "name": "sports_seasons_sport_id_sports_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "auto" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "single_elimination_playoff", + "page_playoff", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0022_snapshot.json b/drizzle/meta/0022_snapshot.json new file mode 100644 index 0000000..25274d6 --- /dev/null +++ b/drizzle/meta/0022_snapshot.json @@ -0,0 +1,2699 @@ +{ + "id": "a01986c0-28e0-46a8-8a25-f599d06bcc00", + "prevId": "122d89a4-2d5c-4a52-8aa9-aa507315831c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_participants_id_fk": { + "name": "draft_picks_participant_id_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_participants_id_fk": { + "name": "draft_queue_participant_id_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_participant_id_participants_id_fk": { + "name": "event_results_participant_id_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_expected_values": { + "name": "participant_expected_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_expected_values_participant_id_participants_id_fk": { + "name": "participant_expected_values_participant_id_participants_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_expected_values_season_id_seasons_id_fk": { + "name": "participant_expected_values_season_id_seasons_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_qualifying_totals": { + "name": "participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_qualifying_totals_participant_id_participants_id_fk": { + "name": "participant_qualifying_totals_participant_id_participants_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_results": { + "name": "participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_results_participant_id_participants_id_fk": { + "name": "participant_results_participant_id_participants_id_fk", + "tableFrom": "participant_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_season_results_participant_id_participants_id_fk": { + "name": "participant_season_results_participant_id_participants_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_season_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_season_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participants": { + "name": "participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participants_sports_season_id_sports_seasons_id_fk": { + "name": "participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_matches": { + "name": "playoff_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "match_number": { + "name": "match_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "loser_id": { + "name": "loser_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_matches_scoring_event_id_scoring_events_id_fk": { + "name": "playoff_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_matches_participant1_id_participants_id_fk": { + "name": "playoff_matches_participant1_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_participant2_id_participants_id_fk": { + "name": "playoff_matches_participant2_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_winner_id_participants_id_fk": { + "name": "playoff_matches_winner_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_loser_id_participants_id_fk": { + "name": "playoff_matches_loser_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "loser_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifying_point_config": { + "name": "qualifying_point_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "qualifying_point_config_sports_season_id_sports_seasons_id_fk": { + "name": "qualifying_point_config_sports_season_id_sports_seasons_id_fk", + "tableFrom": "qualifying_point_config", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scoring_events_sports_season_id_sports_seasons_id_fk": { + "name": "scoring_events_sports_season_id_sports_seasons_id_fk", + "tableFrom": "scoring_events", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_template_sports_template_id_season_templates_id_fk": { + "name": "season_template_sports_template_id_season_templates_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_template_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_template_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_templates": { + "name": "season_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "draft_rounds": { + "name": "draft_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "flex_spots": { + "name": "flex_spots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draft_date_time": { + "name": "draft_date_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "scoring_type": { + "name": "scoring_type", + "type": "scoring_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "scoring_pattern": { + "name": "scoring_pattern", + "type": "scoring_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_majors": { + "name": "total_majors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "majors_completed": { + "name": "majors_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points_finalized": { + "name": "qualifying_points_finalized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sports_seasons_sport_id_sports_id_fk": { + "name": "sports_seasons_sport_id_sports_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "auto" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "single_elimination_playoff", + "page_playoff", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d733da2..9cc44ec 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -148,6 +148,20 @@ "when": 1761719248881, "tag": "0020_slim_spacker_dave", "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1762190473418, + "tag": "0021_fair_ser_duncan", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1762191156317, + "tag": "0022_shiny_mother_askani", + "breakpoints": true } ] } \ No newline at end of file diff --git a/plans/bracket-expansion.md b/plans/bracket-expansion.md new file mode 100644 index 0000000..2418374 --- /dev/null +++ b/plans/bracket-expansion.md @@ -0,0 +1,345 @@ +# Bracket Expansion Plan + +## Problem Statement + +Current bracket system only supports 4 and 8 team single-elimination brackets. We need to support: +- **Standard brackets**: 4, 8, 16, 32 teams +- **March Madness**: 68 teams with unique structure + - First Four: 4 play-in games (8 teams) + - Round of 64: 32 games (64 teams including First Four winners) + - Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship + - **Only Elite Eight and beyond score fantasy points** (per Q18) + - Play-in teams can be any seeds (e.g., 11a vs 11b, 16a vs 16b), not just bottom seeds + +## Current Implementation + +**What we have:** +- `generateSingleEliminationBracket(eventId, participantIds[])` +- Supports 4 or 8 teams only +- Creates matches for all rounds automatically +- Simple sequential seeding (1 vs 2, 3 vs 4, etc.) + +**Limitations:** +- Fixed bracket sizes (4, 8) +- No play-in game support +- No concept of "scoring rounds" vs "non-scoring rounds" +- No flexible seeding/matchup configuration + +## Proposed Solution: Hybrid Approach + +### Option 1: Template-Based Bracket System ✅ **RECOMMENDED** + +Create pre-defined bracket templates for common structures, with manual override capability. + +#### Bracket Templates + +```typescript +interface BracketTemplate { + id: string; + name: string; + totalTeams: number; + rounds: BracketRound[]; + scoringStartsAtRound?: string; // e.g., "Elite Eight" for March Madness +} + +interface BracketRound { + name: string; // "First Four", "Round of 64", "Elite Eight", etc. + matchCount: number; + feedsInto?: string; // Which round winners advance to + isScoring: boolean; // Does this round affect fantasy points? +} + +// Example templates: +const BRACKET_TEMPLATES = { + NCAA_68: { + id: 'ncaa_68', + name: 'NCAA March Madness (68 teams)', + totalTeams: 68, + scoringStartsAtRound: 'Elite Eight', + rounds: [ + { name: 'First Four', matchCount: 4, feedsInto: 'Round of 64', isScoring: false }, + { name: 'Round of 64', matchCount: 32, feedsInto: 'Round of 32', isScoring: false }, + { name: 'Round of 32', matchCount: 16, feedsInto: 'Sweet Sixteen', isScoring: false }, + { name: 'Sweet Sixteen', matchCount: 8, feedsInto: 'Elite Eight', isScoring: false }, + { name: 'Elite Eight', matchCount: 4, feedsInto: 'Final Four', isScoring: true }, // 8 teams, share 5-8th + { name: 'Final Four', matchCount: 2, feedsInto: 'Championship', isScoring: true }, // Losers share 3-4th + { name: 'Championship', matchCount: 1, feedsInto: null, isScoring: true }, // Winner 1st, Loser 2nd + ], + }, + + NFL_14: { + id: 'nfl_14', + name: 'NFL Playoffs (14 teams)', + totalTeams: 14, + scoringStartsAtRound: 'Wild Card', + rounds: [ + { name: 'Wild Card', matchCount: 6, feedsInto: 'Divisional', isScoring: false }, // 2 teams get bye + { name: 'Divisional', matchCount: 4, feedsInto: 'Conference Championship', isScoring: true }, // QF, share 5-8th + { name: 'Conference Championship', matchCount: 2, feedsInto: 'Super Bowl', isScoring: true }, // SF, share 3-4th + { name: 'Super Bowl', matchCount: 1, feedsInto: null, isScoring: true }, // Finals, 1st/2nd + ], + }, + + NBA_16: { + id: 'nba_16', + name: 'NBA Playoffs (16 teams)', + totalTeams: 16, + scoringStartsAtRound: 'Conference Quarterfinals', + rounds: [ + { name: 'First Round', matchCount: 8, feedsInto: 'Conference Semifinals', isScoring: false }, + { name: 'Conference Semifinals', matchCount: 4, feedsInto: 'Conference Finals', isScoring: true }, // QF, share 5-8th + { name: 'Conference Finals', matchCount: 2, feedsInto: 'NBA Finals', isScoring: true }, // SF, share 3-4th + { name: 'NBA Finals', matchCount: 1, feedsInto: null, isScoring: true }, // Finals, 1st/2nd + ], + }, + + SIMPLE_16: { + id: 'simple_16', + name: 'Simple 16-Team Bracket', + totalTeams: 16, + scoringStartsAtRound: 'Quarterfinals', + rounds: [ + { name: 'Round of 16', matchCount: 8, feedsInto: 'Quarterfinals', isScoring: false }, + { name: 'Quarterfinals', matchCount: 4, feedsInto: 'Semifinals', isScoring: true }, // Share 5-8th + { name: 'Semifinals', matchCount: 2, feedsInto: 'Finals', isScoring: true }, // Share 3-4th + { name: 'Finals', matchCount: 1, feedsInto: null, isScoring: true }, // 1st/2nd + ], + }, + + SIMPLE_32: { + id: 'simple_32', + name: 'Simple 32-Team Bracket', + totalTeams: 32, + scoringStartsAtRound: 'Quarterfinals', + rounds: [ + { name: 'Round of 32', matchCount: 16, feedsInto: 'Round of 16', isScoring: false }, + { name: 'Round of 16', matchCount: 8, feedsInto: 'Quarterfinals', isScoring: false }, + { name: 'Quarterfinals', matchCount: 4, feedsInto: 'Semifinals', isScoring: true }, + { name: 'Semifinals', matchCount: 2, feedsInto: 'Finals', isScoring: true }, + { name: 'Finals', matchCount: 1, feedsInto: null, isScoring: true }, + ], + }, +}; +``` + +#### UI Flow + +**Step 1: Select Template** +``` +When creating a bracket event: +1. Admin selects bracket template from dropdown +2. Shows preview of rounds and scoring structure +3. Option to customize (add/remove rounds, change scoring start point) +``` + +**Step 2: Assign Participants** +``` +For March Madness (68 teams): +- Show all 68 slots grouped by round +- First Four: 8 slots (4 matchups) - manual seeding (e.g., "11a vs 11b", "16a vs 16b") +- Round of 64: 60 slots (remaining teams) + 4 TBD (from First Four winners) +- Admin can assign any participant to any slot + +For simpler brackets (4, 8, 16): +- Linear list of slots +- Drag-and-drop or dropdown selection +- Traditional seeding (1 vs 16, 2 vs 15, etc.) +``` + +**Step 3: Generate Bracket** +``` +System creates: +- All playoff_matches records for all rounds +- Properly linked feedsInto relationships +- Marks which rounds contribute to scoring +- Sets initial participant assignments +``` + +#### Database Schema Additions + +```typescript +// Add to playoff_matches table +playoff_matches { + // ... existing fields + + // NEW FIELDS: + isScoring: boolean (default: true) // Does this match affect fantasy points? + templateRound: varchar(50) // "First Four", "Round of 64", etc. + seedInfo: varchar(50) // "1 vs 16", "11a vs 11b", etc. (for display) +} + +// Add to scoring_events table +scoring_events { + // ... existing fields + + // NEW FIELDS: + bracketTemplateId: varchar(50) // "ncaa_68", "nfl_14", etc. + scoringStartsAtRound: varchar(50) // Which round starts fantasy scoring +} +``` + +#### Scoring Logic Updates + +```typescript +/** + * Process playoff event completion - updated for template system + */ +async function processPlayoffEvent(eventId: string) { + const event = await getScoringEventById(eventId); + const matches = await findPlayoffMatchesByEventId(eventId); + + // Filter to only scoring matches + const scoringMatches = matches.filter(m => m.isScoring); + + // Determine which placements to assign based on templateRound + // Elite Eight losers (4 teams) -> share 5-8th + // Final Four losers (2 teams) -> share 3-4th + // Championship loser (1 team) -> 2nd + // Championship winner (1 team) -> 1st + + // For non-scoring rounds: assign 0 points (per Q20) + const nonScoringLosers = matches + .filter(m => !m.isScoring && m.loserId) + .map(m => m.loserId); + + for (const loserId of nonScoringLosers) { + await setParticipantResult(loserId, event.sportsSeasonId, null, 0); + } +} +``` + +#### Admin UI Changes + +**Bracket Generation Page:** +```tsx + + + Select Bracket Template + + + + + {/* Show template details when selected */} +
+

Rounds:

+ {selectedTemplate.rounds.map(round => ( +
+ {round.name} - {round.matchCount} matches + {round.isScoring && Scoring Round} +
+ ))} +
+
+
+ + + + Assign Participants ({templateTeamCount} slots) + + + {/* For March Madness: group by round */} + {template.id === 'ncaa_68' ? ( + <> +

First Four (Play-in Games)

+ {/* 4 matchups, 8 slots with seed labels */} + + +

Round of 64

+ {/* 28 direct assignments + 4 TBD from First Four */} + + + ) : ( + /* Simple linear assignment for other brackets */ + + )} +
+
+``` + +### Implementation Phases + +**Phase 2.6: Template System Foundation** +- [ ] Define bracket template type and constants +- [ ] Add `isScoring` and `templateRound` to playoff_matches schema +- [ ] Add `bracketTemplateId` and `scoringStartsAtRound` to scoring_events +- [ ] Create database migration +- [ ] Update playoff-match model to support templates + +**Phase 2.7: Simple Template Expansion (4, 8, 16, 32)** +- [ ] Create template definitions for 4, 8, 16, 32 team brackets +- [ ] Update `generateBracketFromTemplate()` function +- [ ] Update bracket generation UI to use template selector +- [ ] Test with 16-team and 32-team brackets +- [ ] Update scoring calculator for non-scoring rounds (0 points) + +**Phase 2.8: March Madness Template (68 teams)** +- [ ] Create NCAA_68 template definition +- [ ] Build First Four matchup assignment UI +- [ ] Build Round of 64 with TBD slot handling +- [ ] Implement winner advancement from First Four to Round of 64 +- [ ] Test complete 68-team bracket flow +- [ ] Verify Elite Eight scoring starts correctly + +**Phase 2.9: NFL/NBA Templates** +- [ ] Create NFL_14 template (with bye weeks) +- [ ] Create NBA_16 template +- [ ] Handle bye week logic (teams skip first round) +- [ ] Test both templates + +## Alternative Considered: Manual Match Creation + +**Pros:** +- Maximum flexibility +- Works for any structure + +**Cons:** +- Too complex for admins +- Error-prone (easy to miss connections) +- No validation of bracket structure +- Time-consuming for large brackets + +**Verdict:** Not recommended. Templates provide better UX with flexibility where needed. + +## Open Questions + +1. **Custom Templates**: Should admins be able to save custom bracket templates for reuse? + - **Recommendation**: Not in initial implementation. Add if requested. + +2. **Seed Validation**: Should system validate proper bracket structure (e.g., 1 seed plays 16 seed)? + - **Recommendation**: No. Allow flexible seeding for play-ins and special cases. + +3. **Series Support**: Should we support best-of-7 series (NBA/NHL)? + - **Recommendation**: Not initially. Just track series winner for now. + +4. **Bracket Visualization**: Show traditional bracket tree diagram? + - **Recommendation**: Phase 3 enhancement. Current table view sufficient for Phase 2. + +## Success Criteria + +✅ Admin can create 4, 8, 16, 32 team brackets +✅ Admin can create March Madness 68-team bracket with First Four +✅ Admin can assign any participant to any slot (flexible seeding) +✅ System correctly identifies which rounds score fantasy points +✅ Participants eliminated in non-scoring rounds get 0 points +✅ Elite Eight and beyond calculate placements correctly +✅ Winner advancement works across all bracket sizes diff --git a/plans/phase2-bugs-review.md b/plans/phase2-bugs-review.md new file mode 100644 index 0000000..7abecc1 --- /dev/null +++ b/plans/phase2-bugs-review.md @@ -0,0 +1,337 @@ +# Phase 2 Code Review - Bug Report + +## 🔴 CRITICAL BUGS + +### Bug #1: Winner Advancement Logic Is Incorrect +**File:** `app/models/playoff-match.ts:205-247` + +**Issue:** The `advanceWinner()` function uses "fill participant1 first, then participant2" logic, which doesn't guarantee proper bracket structure. + +**Current Code:** +```typescript +// Determine which participant slot to fill +// For simplicity, fill participant1 first, then participant2 +if (!nextMatch.participant1Id) { + await updatePlayoffMatch(nextMatch.id, { participant1Id: winnerId }); +} else if (!nextMatch.participant2Id) { + await updatePlayoffMatch(nextMatch.id, { participant2Id: winnerId }); +} +``` + +**Problem:** +For Quarterfinals → Semifinals: +- QF Match 1 winner should → SF Match 1, participant1 +- QF Match 2 winner should → SF Match 1, participant2 +- QF Match 3 winner should → SF Match 2, participant1 +- QF Match 4 winner should → SF Match 2, participant2 + +But with current logic: +- QF Match 1 winner → SF Match 1, participant1 ✅ +- QF Match 2 winner → SF Match 1, participant2 ✅ +- QF Match 3 winner → SF Match 2, participant1 ✅ +- QF Match 4 winner → SF Match 2, participant2 ✅ + +Actually, this WORKS for simple cases, but breaks if matches complete out of order: +- If QF Match 2 completes before Match 1, winner goes to SF1 participant1 +- Then QF Match 1 winner goes to SF1 participant2 +- **This reverses the bracket structure!** + +**Fix:** +```typescript +// Determine which participant slot to fill based on match number +if (match.round === "Quarterfinals") { + nextRound = "Semifinals"; + nextMatchNumber = match.matchNumber <= 2 ? 1 : 2; + + // Odd matches (1, 3) → participant1, Even matches (2, 4) → participant2 + const participantSlot = match.matchNumber % 2 === 1 ? 'participant1Id' : 'participant2Id'; + + await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId }); +} else if (match.round === "Semifinals") { + nextRound = "Finals"; + nextMatchNumber = 1; + + // SF Match 1 → Finals participant1, SF Match 2 → Finals participant2 + const participantSlot = match.matchNumber === 1 ? 'participant1Id' : 'participant2Id'; + + await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId }); +} +``` + +**Impact:** Bracket structure breaks if matches complete out of order. Semifinals and Finals may have wrong matchups. + +**Covered in Future Plans?** Yes, bracket-expansion.md will rewrite this entirely with template system. + +--- + +### Bug #2: Multiple Fantasy Leagues with Different Scoring Rules +**File:** `app/models/scoring-calculator.ts:66-74` + +**Issue:** When processing playoff events, we only use the FIRST fantasy season's scoring rules, even if multiple fantasy leagues with different rules use the same sports season. + +**Current Code:** +```typescript +const seasonSport = event.sportsSeason.seasonSports[0]; +if (!seasonSport) { + throw new Error(`No fantasy seasons found for sports season ${event.sportsSeasonId}`); +} + +const scoringRules = await getScoringRules(seasonSport.seasonId, db); +``` + +**Problem:** +Scenario: +- League A uses "2024 NFL Playoffs" with scoring: 1st=100, 2nd=70 +- League B uses "2024 NFL Playoffs" with scoring: 1st=200, 2nd=150 + +When Super Bowl completes: +- We calculate points using League A's rules (100 pts) +- Store `pointsAwarded: 100` in `participantResults` +- League B also sees 100 pts instead of 200 pts + +**Actually Not a Bug (Misleading Design):** +Upon further review, `pointsAwarded` in `participantResults` is stored but **never used**! + +In `calculateTeamScore()` (line 294): +```typescript +const points = calculateFantasyPoints(result.finalPosition, scoringRules); +``` + +It recalculates points from `finalPosition` using each fantasy season's own scoring rules. + +**The Real Issues:** +1. **Confusing Design**: We store `pointsAwarded` but never use it +2. **Wasted Storage**: Storing incorrect values that are never read +3. **Potential Future Bug**: Someone might assume `pointsAwarded` is correct and use it + +**Recommendation:** +Either: +- **Option A**: Stop storing `pointsAwarded` entirely (per plans/scoring-system.md line 341) +- **Option B**: Document clearly that `pointsAwarded` is for admin display only, not calculations + +**Impact:** Currently none (not used), but high risk of future bugs. + +**Covered in Future Plans?** Not explicitly mentioned. This is a design cleanup issue. + +--- + +## 🟡 MODERATE BUGS + +### Bug #3: processPlayoffEvent Assumes Only One Round Per Event +**File:** `app/models/scoring-calculator.ts:25-136` + +**Issue:** The function processes matches based on `event.playoffRound`, but a scoring event might have multiple rounds of matches. + +**Current Design:** +- One scoring event per round (Quarterfinals event, Semifinals event, Finals event) +- Admin must "Complete Round" separately for each + +**Problem:** +If admin creates one event for entire tournament and adds all matches to it, the scoring won't work correctly. We'd need to know which round we're processing. + +**Actually Not a Bug (By Design):** +Looking at the UI flow in `admin.sports-seasons.$id.events.$eventId.bracket.server.ts:178-180`: +```typescript +// We need to temporarily update the event's playoffRound to the one being completed +await updateScoringEvent(params.eventId, { playoffRound: round }); +``` + +The system updates the event's `playoffRound` field when completing each round, so this works. + +**Potential Issue:** +What if admin completes rounds out of order? (Completes Finals before Semifinals?) + +Current code would allow it - no validation that rounds are completed in sequence. + +**Impact:** Low - admin would have to intentionally do something wrong. + +**Covered in Future Plans?** bracket-expansion.md will address this with template system. + +--- + +### Bug #4: No Validation for Completing Rounds Out of Order +**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:144-206` + +**Issue:** Admin can complete rounds in any order (Finals before Quarterfinals, etc.) + +**Current Code:** +```typescript +// Verify all matches in this round are complete +const roundMatches = matches.filter((m) => m.round === round); +const allComplete = roundMatches.every((m) => m.isComplete); +if (!allComplete) { + return { error: `Not all matches in ${round} are complete` }; +} +``` + +Only checks if matches in the selected round are complete, not whether previous rounds are complete. + +**Problem:** +1. Admin completes Finals first (assigns 1st and 2nd place) +2. Later completes Semifinals (assigns 3rd and 4th place) +3. **Participants now have multiple placement records!** + +Actually, this might work because `upsertParticipantResult` updates existing records. But it's confusing and could lead to data inconsistency. + +**Fix:** +Add validation to ensure rounds are completed in order (QF → SF → Finals). + +**Impact:** Moderate - could cause confused admin experience and potentially wrong data. + +**Covered in Future Plans?** Not explicitly, but template system might make this clearer. + +--- + +### Bug #5: Decimal Point Handling May Cause Precision Issues +**File:** `app/models/playoff-match.ts:125-126`, `database/schema.ts:participant1Score` + +**Issue:** Scores are stored as decimal in database but converted to/from string. + +**Current Code:** +```typescript +participant1Score: participant1Score?.toString(), +participant2Score: participant2Score?.toString(), +``` + +**Database Schema:** +```typescript +participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }) +``` + +**Problem:** +When reading scores back, we get strings: `"27.50"` not numbers: `27.5` + +The UI displays: `parseFloat(match.participant1Score)` but this could show `27.5` instead of `27.50` (minor cosmetic issue). + +**Impact:** Very low - cosmetic only for score display. + +**Fix:** Parse to float when displaying, or use proper decimal type handling. + +--- + +## 🟢 MINOR ISSUES / CODE QUALITY + +### Issue #1: Unused Import in complete-round Action +**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:183-191` + +**Code:** +```typescript +const { findSeasonSportsBySportsSeasonId } = await import("~/models/season-sport"); +const seasonSports = await findSeasonSportsBySportsSeasonId(params.id); +if (seasonSports.length === 0) { + return { error: "No fantasy seasons found for this sports season" }; +} +``` + +**Issue:** We check if seasonSports exist but never use them. The check is good validation, but we could simplify. + +**Impact:** None - just unnecessary code. + +--- + +### Issue #2: Error Swallowing in advanceWinner +**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:124-132` + +**Code:** +```typescript +try { + await advanceWinner(matchId, winnerId); +} catch (error) { + // It's ok if advancement fails (e.g., next match already filled) + console.warn("Could not advance winner:", error); +} +``` + +**Issue:** Silently swallows all advancement errors, including real bugs. + +**Better Approach:** +```typescript +try { + await advanceWinner(matchId, winnerId); +} catch (error) { + // Only ignore "already filled" errors + if (error instanceof Error && error.message.includes("already has both participants")) { + console.warn("Match already filled, skipping advancement"); + } else { + throw error; // Re-throw unexpected errors + } +} +``` + +**Impact:** Bugs in advancement logic might be hidden. + +--- + +### Issue #3: Magic Numbers in Bracket Generation +**File:** `app/models/playoff-match.ts:220-226` + +**Code:** +```typescript +if (match.round === "Quarterfinals") { + nextRound = "Semifinals"; + // Match 1,2 -> SF1, Match 3,4 -> SF2 + nextMatchNumber = match.matchNumber <= 2 ? 1 : 2; +} +``` + +**Issue:** Hard-coded bracket structure won't work for larger brackets. + +**Impact:** Will be rewritten in bracket-expansion.md anyway. + +--- + +## 📊 SUMMARY + +| Severity | Count | Critical? | +|----------|-------|-----------| +| 🔴 Critical | 2 | Bug #1 (advancement), Bug #2 (scoring rules - design issue) | +| 🟡 Moderate | 3 | Bugs #3, #4, #5 | +| 🟢 Minor | 3 | Issues #1, #2, #3 | + +### Recommendations + +**Immediate Fixes Needed:** +1. ✅ **Fix Bug #1** (Winner advancement) - High priority, breaks brackets +2. ✅ **Fix Bug #4** (Round order validation) - Prevents data corruption + +**Design Decisions Needed:** +3. **Bug #2** (pointsAwarded) - Decide: Remove field or document it's unused? +4. **Issue #2** (Error swallowing) - Better error handling + +**Can Wait for Phase 2.6+ (Bracket Expansion):** +5. Bug #3 (multiple rounds) - Will be fixed by template system +6. Bug #5 (decimals) - Cosmetic only +7. Issue #1 (unused validation) - Minor cleanup +8. Issue #3 (magic numbers) - Will be rewritten + +### Comparison with Plans + +**From scoring-system.md:** +> "pointsAwarded: Calculated based on league scoring rules (NOT stored here - calculated on demand)" + +**Current Implementation:** We ARE storing it but not using it. ❌ Not following plan. + +**From bracket-expansion.md:** +> "Phase 2.6: Template System Foundation" + +**Impact on Bugs:** Bugs #1 and #3 will be completely rewritten, so fixing them now is optional if Phase 2.6 is coming soon. + +### Decision Point + +**Should we fix bugs now or wait for bracket-expansion?** + +**Fix Now:** +- Bug #1 (advancement) - 30 min fix +- Bug #4 (round order) - 15 min fix +- Bug #2 decision (remove pointsAwarded field) - Requires migration + +**Total Time:** ~1 hour + migration + +**Wait for Phase 2.6:** +- Bracket expansion rewrites most of this code anyway +- Could waste time fixing code that's about to be replaced + +**My Recommendation:** +1. Fix Bug #4 (round order validation) now - prevents data corruption +2. Document Bug #1 and #2, wait for bracket-expansion to fix properly +3. Make decision on pointsAwarded field (keep but document, or remove entirely) diff --git a/plans/scoring-system.md b/plans/scoring-system.md index 0258dba..22fd967 100644 --- a/plans/scoring-system.md +++ b/plans/scoring-system.md @@ -962,42 +962,55 @@ All clarification questions (Q1-Q21) have been answered and confirmed: - [x] **1.4** Basic admin result entry UI ✅ *Completed* - [x] Create admin route structure for sports seasons - [x] List scoring events for a sports season - - [x] Create new event form + - [x] Create new event form (playoff_game, major_tournament, race, final_standings) - [x] Basic result entry form (placement only) - - [x] Update existing results (inline editing) + - [x] Update existing results (inline editing with Enter key support) - [x] Delete results with confirmation + - [x] Delete scoring events with confirmation + - [x] Proper .server.ts file separation for client/server code + - [x] Navigation from sports season → events list → event details -### Phase 2: Playoff Scoring (Single Elimination) +### Phase 2: Playoff Scoring (Single Elimination) ✅ *Completed* **Goal**: Implement playoff bracket tracking and scoring for NFL/NBA/MLB -- [ ] **2.1** Playoff event creation - - [ ] UI for creating playoff rounds (QF, SF, Finals) - - [ ] Bracket structure definition - - [ ] Match creation (automatic from bracket structure) +- [x] **2.1** Playoff event creation ✅ + - [x] UI for creating playoff rounds (QF, SF, Finals) + - [x] Bracket structure definition (4-team and 8-team brackets) + - [x] Match creation (automatic from bracket structure) + - [x] Created playoff-match model with bracket generation + - [x] Created bracket UI route at `/admin/sports-seasons/:id/events/:eventId/bracket` -- [ ] **2.2** Bracket match tracking - - [ ] UI for entering match results (winner/loser) - - [ ] Display bracket structure - - [ ] Show matchup participants +- [x] **2.2** Bracket match tracking ✅ + - [x] UI for entering match results (winner/loser) + - [x] Display bracket structure grouped by round + - [x] Show matchup participants with TBD placeholders + - [x] Automatic winner advancement to next round + - [x] Added "Manage Bracket" button to event details page -- [ ] **2.3** Playoff scoring calculation - - [ ] Implement placement sharing logic (Q19) - - [ ] Calculate averaged points for shared placements - - [ ] Handle multi-way ties (4-way for QF losers) - - [ ] Update `participantResults` with final placements - - [ ] Implement 0-point recording for early eliminations (Q20) +- [x] **2.3** Playoff scoring calculation ✅ + - [x] Implement placement sharing logic (Q19) + - [x] Calculate averaged points for shared placements + - [x] Handle multi-way ties (4-way for QF losers, 2-way for SF losers) + - [x] Update `participantResults` with final placements + - [x] Implement 0-point recording for early eliminations (Q20) + - [x] Created `processPlayoffEvent` function in scoring-calculator + - [x] Support for Quarterfinals (5th-8th shared), Semifinals (3rd-4th shared), Finals (1st, 2nd) -- [ ] **2.4** Basic standings display - - [ ] Create `StandingsTable` component - - [ ] Show total points, rank, movement - - [ ] Show participants remaining count - - [ ] League home page standings integration +- [x] **2.4** Basic standings display ✅ + - [x] Create `StandingsTable` component + - [x] Show total points, rank, movement indicators + - [x] Show participants remaining count + - [x] Display placement breakdown (1st-8th counts) for tiebreakers + - [x] Rank badges with trophy/medal icons for top 3 + - [x] League home page standings integration (component ready, route integration pending) -- [ ] **2.5** Testing with real data - - [ ] Create test NFL playoff bracket - - [ ] Enter sample results - - [ ] Verify point calculations - - [ ] Test tie scenarios +- [x] **2.5** Testing with real data ✅ + - [x] Created comprehensive test suite (18 tests) + - [x] Test NFL playoff scenarios (8-team bracket) + - [x] Test NCAA March Madness Elite Eight scenarios + - [x] Verify point calculations for all rounds + - [x] Test tie scenarios (2-way, 4-way, edge cases) + - [x] All tests passing ✅ ### Phase 3: Other Scoring Patterns **Goal**: Implement season standings (F1) and qualifying points (Golf/Tennis)