From da1a17f2d33f5c8218a137ca7dc710da796a9b81 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Mon, 3 Nov 2025 13:16:37 -0800 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- app/lib/bracket-templates.ts | 348 +++ .../__tests__/bracket-templates.test.ts | 202 ++ app/models/playoff-match.ts | 128 + app/models/scoring-event.ts | 12 +- ...sons.$id.events.$eventId.bracket.server.ts | 14 +- .../admin.sports-seasons.$id.events.server.ts | 2 - .../admin.sports-seasons.$id.events.tsx | 39 +- database/schema.ts | 7 + drizzle/0023_cynical_jack_power.sql | 5 + drizzle/meta/0023_snapshot.json | 2730 +++++++++++++++++ drizzle/meta/_journal.json | 7 + plans/scoring-system.md | 130 +- 12 files changed, 3588 insertions(+), 36 deletions(-) create mode 100644 app/lib/bracket-templates.ts create mode 100644 app/models/__tests__/bracket-templates.test.ts create mode 100644 drizzle/0023_cynical_jack_power.sql create mode 100644 drizzle/meta/0023_snapshot.json diff --git a/app/lib/bracket-templates.ts b/app/lib/bracket-templates.ts new file mode 100644 index 0000000..71fdefc --- /dev/null +++ b/app/lib/bracket-templates.ts @@ -0,0 +1,348 @@ +/** + * Bracket Template System + * + * Defines pre-configured tournament bracket structures for various sports. + * Templates specify rounds, match counts, and which rounds contribute to fantasy scoring. + */ + +export interface BracketRound { + /** Display name for this round (e.g., "First Four", "Elite Eight") */ + name: string; + /** Number of matches in this round */ + matchCount: number; + /** Name of the round that winners advance to (null for championship) */ + feedsInto: string | null; + /** Whether this round affects fantasy points (false = 0 points per Q20) */ + isScoring: boolean; +} + +export interface BracketTemplate { + /** Unique identifier for this template */ + id: string; + /** Human-readable name */ + name: string; + /** Total number of teams in the bracket */ + totalTeams: number; + /** Ordered list of rounds from earliest to championship */ + rounds: BracketRound[]; + /** Round name where fantasy scoring begins */ + scoringStartsAtRound: string; +} + +/** + * Simple 4-team bracket + * Semifinals → Finals + * All rounds score fantasy points + */ +export const SIMPLE_4: BracketTemplate = { + id: "simple_4", + name: "Simple 4-Team Bracket", + totalTeams: 4, + scoringStartsAtRound: "Semifinals", + rounds: [ + { + name: "Semifinals", + matchCount: 2, + feedsInto: "Finals", + isScoring: true, // Losers share 3rd-4th + }, + { + name: "Finals", + matchCount: 1, + feedsInto: null, + isScoring: true, // Winner 1st, Loser 2nd + }, + ], +}; + +/** + * Simple 8-team bracket + * Quarterfinals → Semifinals → Finals + * All rounds score fantasy points + */ +export const SIMPLE_8: BracketTemplate = { + id: "simple_8", + name: "Simple 8-Team Bracket", + totalTeams: 8, + scoringStartsAtRound: "Quarterfinals", + rounds: [ + { + name: "Quarterfinals", + matchCount: 4, + feedsInto: "Semifinals", + isScoring: true, // Losers share 5th-8th + }, + { + name: "Semifinals", + matchCount: 2, + feedsInto: "Finals", + isScoring: true, // Losers share 3rd-4th + }, + { + name: "Finals", + matchCount: 1, + feedsInto: null, + isScoring: true, // Winner 1st, Loser 2nd + }, + ], +}; + +/** + * Simple 16-team bracket + * Round of 16 → Quarterfinals → Semifinals → Finals + * Only Quarterfinals and beyond score points (top 8) + */ +export const SIMPLE_16: BracketTemplate = { + id: "simple_16", + name: "Simple 16-Team Bracket", + totalTeams: 16, + scoringStartsAtRound: "Quarterfinals", + rounds: [ + { + name: "Round of 16", + matchCount: 8, + feedsInto: "Quarterfinals", + isScoring: false, // Early elimination = 0 points + }, + { + name: "Quarterfinals", + matchCount: 4, + feedsInto: "Semifinals", + isScoring: true, // Losers share 5th-8th + }, + { + name: "Semifinals", + matchCount: 2, + feedsInto: "Finals", + isScoring: true, // Losers share 3rd-4th + }, + { + name: "Finals", + matchCount: 1, + feedsInto: null, + isScoring: true, // Winner 1st, Loser 2nd + }, + ], +}; + +/** + * Simple 32-team bracket + * Round of 32 → Round of 16 → Quarterfinals → Semifinals → Finals + * Only Quarterfinals and beyond score points (top 8) + */ +export const SIMPLE_32: BracketTemplate = { + 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, // Early elimination = 0 points + }, + { + name: "Round of 16", + matchCount: 8, + feedsInto: "Quarterfinals", + isScoring: false, // Early elimination = 0 points + }, + { + name: "Quarterfinals", + matchCount: 4, + feedsInto: "Semifinals", + isScoring: true, // Losers share 5th-8th + }, + { + name: "Semifinals", + matchCount: 2, + feedsInto: "Finals", + isScoring: true, // Losers share 3rd-4th + }, + { + name: "Finals", + matchCount: 1, + feedsInto: null, + isScoring: true, // Winner 1st, Loser 2nd + }, + ], +}; + +/** + * NCAA March Madness (68 teams) + * First Four (play-in) → Round of 64 → Round of 32 → Sweet Sixteen → Elite Eight → Final Four → Championship + * Only Elite Eight and beyond score points per Q18 + */ +export const NCAA_68: BracketTemplate = { + 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, // Play-in games don't score + }, + { + 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, // Losers share 5th-8th + }, + { + name: "Final Four", + matchCount: 2, + feedsInto: "Championship", + isScoring: true, // Losers share 3rd-4th + }, + { + name: "Championship", + matchCount: 1, + feedsInto: null, + isScoring: true, // Winner 1st, Loser 2nd + }, + ], +}; + +/** + * NFL Playoffs (14 teams) + * Wild Card → Divisional → Conference Championship → Super Bowl + * Top 2 seeds get byes (skip Wild Card) + * Scoring starts at Divisional (top 8) + */ +export const NFL_14: BracketTemplate = { + id: "nfl_14", + name: "NFL Playoffs (14 teams)", + totalTeams: 14, + scoringStartsAtRound: "Divisional", + rounds: [ + { + name: "Wild Card", + matchCount: 6, + feedsInto: "Divisional", + isScoring: false, // 12 teams play, 2 have byes + }, + { + name: "Divisional", + matchCount: 4, + feedsInto: "Conference Championship", + isScoring: true, // Quarterfinals - Losers share 5th-8th + }, + { + name: "Conference Championship", + matchCount: 2, + feedsInto: "Super Bowl", + isScoring: true, // Semifinals - Losers share 3rd-4th + }, + { + name: "Super Bowl", + matchCount: 1, + feedsInto: null, + isScoring: true, // Finals - Winner 1st, Loser 2nd + }, + ], +}; + +/** + * NBA Playoffs (16 teams) + * First Round → Conference Semifinals → Conference Finals → NBA Finals + * Scoring starts at Conference Semifinals (top 8) + */ +export const NBA_16: BracketTemplate = { + id: "nba_16", + name: "NBA Playoffs (16 teams)", + totalTeams: 16, + scoringStartsAtRound: "Conference Semifinals", + rounds: [ + { + name: "First Round", + matchCount: 8, + feedsInto: "Conference Semifinals", + isScoring: false, + }, + { + name: "Conference Semifinals", + matchCount: 4, + feedsInto: "Conference Finals", + isScoring: true, // Quarterfinals - Losers share 5th-8th + }, + { + name: "Conference Finals", + matchCount: 2, + feedsInto: "NBA Finals", + isScoring: true, // Semifinals - Losers share 3rd-4th + }, + { + name: "NBA Finals", + matchCount: 1, + feedsInto: null, + isScoring: true, // Finals - Winner 1st, Loser 2nd + }, + ], +}; + +/** + * All available bracket templates + */ +export const BRACKET_TEMPLATES: Record = { + simple_4: SIMPLE_4, + simple_8: SIMPLE_8, + simple_16: SIMPLE_16, + simple_32: SIMPLE_32, + ncaa_68: NCAA_68, + nfl_14: NFL_14, + nba_16: NBA_16, +}; + +/** + * Get a bracket template by ID + */ +export function getBracketTemplate(id: string): BracketTemplate | undefined { + return BRACKET_TEMPLATES[id]; +} + +/** + * Get all bracket templates as an array + */ +export function getAllBracketTemplates(): BracketTemplate[] { + return Object.values(BRACKET_TEMPLATES); +} + +/** + * Helper to determine scoring round type based on match count in scoring rounds + * Used for determining placement sharing (5-8th, 3-4th, 1-2nd) + */ +export function getScoringRoundType( + roundName: string, + template: BracketTemplate +): "quarterfinals" | "semifinals" | "finals" | null { + const round = template.rounds.find((r) => r.name === roundName); + if (!round || !round.isScoring) return null; + + // Determine type by match count in scoring round + if (round.matchCount === 4) return "quarterfinals"; // 8 teams, losers share 5-8th + if (round.matchCount === 2) return "semifinals"; // 4 teams, losers share 3-4th + if (round.matchCount === 1) return "finals"; // 2 teams, 1st and 2nd + return null; +} diff --git a/app/models/__tests__/bracket-templates.test.ts b/app/models/__tests__/bracket-templates.test.ts new file mode 100644 index 0000000..9b17eda --- /dev/null +++ b/app/models/__tests__/bracket-templates.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + SIMPLE_4, + SIMPLE_8, + SIMPLE_16, + SIMPLE_32, + NCAA_68, + NFL_14, + NBA_16, + getBracketTemplate, + getScoringRoundType, +} from "~/lib/bracket-templates"; + +describe("Bracket Templates", () => { + describe("Template Structure", () => { + it("SIMPLE_4 has correct structure", () => { + expect(SIMPLE_4.id).toBe("simple_4"); + expect(SIMPLE_4.totalTeams).toBe(4); + expect(SIMPLE_4.rounds).toHaveLength(2); + expect(SIMPLE_4.scoringStartsAtRound).toBe("Semifinals"); + expect(SIMPLE_4.rounds.every((r) => r.isScoring)).toBe(true); + }); + + it("SIMPLE_8 has correct structure", () => { + expect(SIMPLE_8.id).toBe("simple_8"); + expect(SIMPLE_8.totalTeams).toBe(8); + expect(SIMPLE_8.rounds).toHaveLength(3); + expect(SIMPLE_8.scoringStartsAtRound).toBe("Quarterfinals"); + expect(SIMPLE_8.rounds.every((r) => r.isScoring)).toBe(true); + }); + + it("SIMPLE_16 has correct structure", () => { + expect(SIMPLE_16.id).toBe("simple_16"); + expect(SIMPLE_16.totalTeams).toBe(16); + expect(SIMPLE_16.rounds).toHaveLength(4); + expect(SIMPLE_16.scoringStartsAtRound).toBe("Quarterfinals"); + + // First round doesn't score + expect(SIMPLE_16.rounds[0].isScoring).toBe(false); + expect(SIMPLE_16.rounds[0].name).toBe("Round of 16"); + + // Quarterfinals and beyond score + expect(SIMPLE_16.rounds[1].isScoring).toBe(true); + expect(SIMPLE_16.rounds[2].isScoring).toBe(true); + expect(SIMPLE_16.rounds[3].isScoring).toBe(true); + }); + + it("SIMPLE_32 has correct structure", () => { + expect(SIMPLE_32.id).toBe("simple_32"); + expect(SIMPLE_32.totalTeams).toBe(32); + expect(SIMPLE_32.rounds).toHaveLength(5); + expect(SIMPLE_32.scoringStartsAtRound).toBe("Quarterfinals"); + + // First two rounds don't score + expect(SIMPLE_32.rounds[0].isScoring).toBe(false); + expect(SIMPLE_32.rounds[1].isScoring).toBe(false); + + // Quarterfinals and beyond score + expect(SIMPLE_32.rounds[2].isScoring).toBe(true); + expect(SIMPLE_32.rounds[3].isScoring).toBe(true); + expect(SIMPLE_32.rounds[4].isScoring).toBe(true); + }); + + it("NCAA_68 has correct structure", () => { + expect(NCAA_68.id).toBe("ncaa_68"); + expect(NCAA_68.totalTeams).toBe(68); + expect(NCAA_68.rounds).toHaveLength(7); + expect(NCAA_68.scoringStartsAtRound).toBe("Elite Eight"); + + // First Four through Sweet Sixteen don't score + expect(NCAA_68.rounds[0].name).toBe("First Four"); + expect(NCAA_68.rounds[0].isScoring).toBe(false); + expect(NCAA_68.rounds[1].name).toBe("Round of 64"); + expect(NCAA_68.rounds[1].isScoring).toBe(false); + expect(NCAA_68.rounds[2].name).toBe("Round of 32"); + expect(NCAA_68.rounds[2].isScoring).toBe(false); + expect(NCAA_68.rounds[3].name).toBe("Sweet Sixteen"); + expect(NCAA_68.rounds[3].isScoring).toBe(false); + + // Elite Eight and beyond score + expect(NCAA_68.rounds[4].name).toBe("Elite Eight"); + expect(NCAA_68.rounds[4].isScoring).toBe(true); + expect(NCAA_68.rounds[5].name).toBe("Final Four"); + expect(NCAA_68.rounds[5].isScoring).toBe(true); + expect(NCAA_68.rounds[6].name).toBe("Championship"); + expect(NCAA_68.rounds[6].isScoring).toBe(true); + }); + + it("NFL_14 has correct structure", () => { + expect(NFL_14.id).toBe("nfl_14"); + expect(NFL_14.totalTeams).toBe(14); + expect(NFL_14.rounds).toHaveLength(4); + expect(NFL_14.scoringStartsAtRound).toBe("Divisional"); + + // Wild Card doesn't score + expect(NFL_14.rounds[0].name).toBe("Wild Card"); + expect(NFL_14.rounds[0].isScoring).toBe(false); + + // Divisional and beyond score + expect(NFL_14.rounds[1].name).toBe("Divisional"); + expect(NFL_14.rounds[1].isScoring).toBe(true); + expect(NFL_14.rounds[2].isScoring).toBe(true); + expect(NFL_14.rounds[3].isScoring).toBe(true); + }); + + it("NBA_16 has correct structure", () => { + expect(NBA_16.id).toBe("nba_16"); + expect(NBA_16.totalTeams).toBe(16); + expect(NBA_16.rounds).toHaveLength(4); + expect(NBA_16.scoringStartsAtRound).toBe("Conference Semifinals"); + + // First Round doesn't score + expect(NBA_16.rounds[0].name).toBe("First Round"); + expect(NBA_16.rounds[0].isScoring).toBe(false); + + // Conference Semifinals and beyond score + expect(NBA_16.rounds[1].name).toBe("Conference Semifinals"); + expect(NBA_16.rounds[1].isScoring).toBe(true); + expect(NBA_16.rounds[2].isScoring).toBe(true); + expect(NBA_16.rounds[3].isScoring).toBe(true); + }); + }); + + describe("Round Advancement", () => { + it("rounds correctly feed into next rounds", () => { + // 8-team bracket + expect(SIMPLE_8.rounds[0].feedsInto).toBe("Semifinals"); + expect(SIMPLE_8.rounds[1].feedsInto).toBe("Finals"); + expect(SIMPLE_8.rounds[2].feedsInto).toBe(null); + + // NCAA bracket + expect(NCAA_68.rounds[0].feedsInto).toBe("Round of 64"); + expect(NCAA_68.rounds[1].feedsInto).toBe("Round of 32"); + expect(NCAA_68.rounds[2].feedsInto).toBe("Sweet Sixteen"); + expect(NCAA_68.rounds[3].feedsInto).toBe("Elite Eight"); + expect(NCAA_68.rounds[4].feedsInto).toBe("Final Four"); + expect(NCAA_68.rounds[5].feedsInto).toBe("Championship"); + expect(NCAA_68.rounds[6].feedsInto).toBe(null); + }); + }); + + describe("Match Counts", () => { + it("calculates correct match counts for each round", () => { + // 16-team bracket + expect(SIMPLE_16.rounds[0].matchCount).toBe(8); // Round of 16 + expect(SIMPLE_16.rounds[1].matchCount).toBe(4); // QF + expect(SIMPLE_16.rounds[2].matchCount).toBe(2); // SF + expect(SIMPLE_16.rounds[3].matchCount).toBe(1); // Finals + + // NCAA bracket + expect(NCAA_68.rounds[0].matchCount).toBe(4); // First Four + expect(NCAA_68.rounds[1].matchCount).toBe(32); // Round of 64 + expect(NCAA_68.rounds[2].matchCount).toBe(16); // Round of 32 + expect(NCAA_68.rounds[3].matchCount).toBe(8); // Sweet Sixteen + expect(NCAA_68.rounds[4].matchCount).toBe(4); // Elite Eight + expect(NCAA_68.rounds[5].matchCount).toBe(2); // Final Four + expect(NCAA_68.rounds[6].matchCount).toBe(1); // Championship + }); + }); + + describe("getBracketTemplate", () => { + it("returns correct template by id", () => { + expect(getBracketTemplate("simple_4")).toBe(SIMPLE_4); + expect(getBracketTemplate("simple_8")).toBe(SIMPLE_8); + expect(getBracketTemplate("simple_16")).toBe(SIMPLE_16); + expect(getBracketTemplate("ncaa_68")).toBe(NCAA_68); + }); + + it("returns undefined for invalid id", () => { + expect(getBracketTemplate("invalid")).toBeUndefined(); + }); + }); + + describe("getScoringRoundType", () => { + it("identifies quarterfinals (4 matches)", () => { + const type = getScoringRoundType("Quarterfinals", SIMPLE_8); + expect(type).toBe("quarterfinals"); + }); + + it("identifies semifinals (2 matches)", () => { + const type = getScoringRoundType("Semifinals", SIMPLE_8); + expect(type).toBe("semifinals"); + }); + + it("identifies finals (1 match)", () => { + const type = getScoringRoundType("Finals", SIMPLE_8); + expect(type).toBe("finals"); + }); + + it("returns null for non-scoring rounds", () => { + const type = getScoringRoundType("Round of 16", SIMPLE_16); + expect(type).toBe(null); + }); + + it("works with NCAA template", () => { + expect(getScoringRoundType("Elite Eight", NCAA_68)).toBe("quarterfinals"); + expect(getScoringRoundType("Final Four", NCAA_68)).toBe("semifinals"); + expect(getScoringRoundType("Championship", NCAA_68)).toBe("finals"); + expect(getScoringRoundType("Sweet Sixteen", NCAA_68)).toBe(null); + }); + }); +}); diff --git a/app/models/playoff-match.ts b/app/models/playoff-match.ts index 6828d84..a918773 100644 --- a/app/models/playoff-match.ts +++ b/app/models/playoff-match.ts @@ -1,6 +1,8 @@ import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; +import type { BracketTemplate } from "~/lib/bracket-templates"; +import { getBracketTemplate } from "~/lib/bracket-templates"; export type PlayoffMatch = typeof schema.playoffMatches.$inferSelect; export type NewPlayoffMatch = typeof schema.playoffMatches.$inferInsert; @@ -249,3 +251,129 @@ export async function advanceWinner( // Fill the determined slot await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId }); } + +/** + * Generate a bracket from a template + * Phase 2.6: Template-based bracket generation + * + * @param eventId - The scoring event ID + * @param templateId - The bracket template ID (e.g., "simple_16", "ncaa_68") + * @param participantIds - Array of participant IDs to assign to the bracket (optional, can be assigned later) + * @returns Array of created playoff matches + */ +export async function generateBracketFromTemplate( + eventId: string, + templateId: string, + participantIds?: string[] +): Promise { + const template = getBracketTemplate(templateId); + if (!template) { + throw new Error(`Bracket template '${templateId}' not found`); + } + + // Validate participant count if provided + if (participantIds && participantIds.length !== template.totalTeams) { + throw new Error( + `Template '${templateId}' requires ${template.totalTeams} participants, but ${participantIds.length} were provided` + ); + } + + const matches: NewPlayoffMatch[] = []; + let participantIndex = 0; + + // Generate matches for each round in the template + for (const round of template.rounds) { + const teamsInRound = round.matchCount * 2; + + for (let i = 0; i < round.matchCount; i++) { + // Determine if we should assign participants to this match + let participant1Id: string | null = null; + let participant2Id: string | null = null; + + // Only assign participants to the first round if participantIds provided + if (participantIds && round === template.rounds[0]) { + participant1Id = participantIds[participantIndex] || null; + participant2Id = participantIds[participantIndex + 1] || null; + participantIndex += 2; + } + + matches.push({ + scoringEventId: eventId, + round: round.name, + matchNumber: i + 1, + participant1Id, + participant2Id, + isComplete: false, + isScoring: round.isScoring, + templateRound: round.name, + seedInfo: null, // Can be set manually later for display (e.g., "1 vs 16") + }); + } + } + + return await createManyPlayoffMatches(matches); +} + +/** + * Advance winner to next round using template-based logic + * Works with any bracket template + */ +export async function advanceWinnerTemplate( + matchId: string, + winnerId: string, + template: BracketTemplate +): Promise { + const db = database(); + + // Get the match + const match = await findPlayoffMatchById(matchId); + if (!match) throw new Error("Match not found"); + + // Find current round in template + const currentRoundIndex = template.rounds.findIndex( + (r) => r.name === match.round + ); + if (currentRoundIndex === -1) { + throw new Error(`Round '${match.round}' not found in template`); + } + + const currentRound = template.rounds[currentRoundIndex]; + + // Check if there's a next round + if (!currentRound.feedsInto) { + // Championship game - no advancement needed + return; + } + + const nextRound = template.rounds.find((r) => r.name === currentRound.feedsInto); + if (!nextRound) { + throw new Error(`Next round '${currentRound.feedsInto}' not found in template`); + } + + // Determine next match number and slot + // This uses the same logic as before: matches advance in sequential pairs + const nextMatchNumber = Math.ceil(match.matchNumber / 2); + const participantSlot: 'participant1Id' | 'participant2Id' = + match.matchNumber % 2 === 1 ? 'participant1Id' : 'participant2Id'; + + // Find the next match + const nextMatches = await findPlayoffMatchesByEventIdAndRound( + match.scoringEventId, + nextRound.name + ); + const nextMatch = nextMatches.find((m) => m.matchNumber === nextMatchNumber); + + if (!nextMatch) { + throw new Error( + `Next match not found: round=${nextRound.name}, matchNumber=${nextMatchNumber}` + ); + } + + // 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-event.ts b/app/models/scoring-event.ts index f4da2fe..b2e9e0f 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -9,16 +9,19 @@ export interface CreateScoringEventData { name: string; eventDate?: Date; eventType: EventType; - playoffRound?: string; // "Quarterfinals", "Semifinals", "Finals" isQualifyingEvent?: boolean; + // Template system fields (Phase 2.6) + bracketTemplateId?: string; + scoringStartsAtRound?: string; } export interface UpdateScoringEventData { name?: string; eventDate?: Date; - playoffRound?: string; isComplete?: boolean; completedAt?: Date; + bracketTemplateId?: string; + scoringStartsAtRound?: string; } /** @@ -37,8 +40,9 @@ export async function createScoringEvent( name: data.name, eventDate: data.eventDate ? data.eventDate.toISOString().split('T')[0] : undefined, eventType: data.eventType, - playoffRound: data.playoffRound, isQualifyingEvent: data.isQualifyingEvent || false, + bracketTemplateId: data.bracketTemplateId, + scoringStartsAtRound: data.scoringStartsAtRound, isComplete: false, }) .returning(); @@ -143,6 +147,8 @@ export async function updateScoringEvent( } if (data.isComplete !== undefined) updateData.isComplete = data.isComplete; if (data.completedAt !== undefined) updateData.completedAt = data.completedAt; + if (data.bracketTemplateId !== undefined) updateData.bracketTemplateId = data.bracketTemplateId; + if (data.scoringStartsAtRound !== undefined) updateData.scoringStartsAtRound = data.scoringStartsAtRound; const [updated] = await db .update(schema.scoringEvents) 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 index 2c23fe7..0835a4a 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -11,6 +11,9 @@ import { findPlayoffMatchById, } from "~/models/playoff-match"; import { processPlayoffEvent } from "~/models/scoring-calculator"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -221,9 +224,14 @@ export async function action({ request, params }: Route.ActionArgs) { } // 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 }); + // TODO Phase 2.7: Refactor processPlayoffEvent to use template system + // For now, we still need to set playoffRound for the old scoring logic + // even though we removed it from the create event UI + const db = database(); + await db + .update(schema.scoringEvents) + .set({ playoffRound: round, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.id, params.eventId)); // Get a fantasy season ID for scoring calculation const { findSeasonSportsBySportsSeasonId } = await import( diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index 5589b5b..882c5b2 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -48,7 +48,6 @@ export async function action({ request, params }: Route.ActionArgs) { const name = formData.get("name"); const eventType = formData.get("eventType"); const eventDate = formData.get("eventDate"); - const playoffRound = formData.get("playoffRound"); // Validation if (typeof name !== "string" || !name.trim()) { @@ -68,7 +67,6 @@ export async function action({ request, params }: Route.ActionArgs) { name: name.trim(), 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, }; try { diff --git a/app/routes/admin.sports-seasons.$id.events.tsx b/app/routes/admin.sports-seasons.$id.events.tsx index 1804334..47251a5 100644 --- a/app/routes/admin.sports-seasons.$id.events.tsx +++ b/app/routes/admin.sports-seasons.$id.events.tsx @@ -91,30 +91,18 @@ export default function SportsSeasonEvents({ /> -
-
- - -
- -
- - -
+
+ +
@@ -151,7 +139,7 @@ export default function SportsSeasonEvents({

) : (
- {events.map((event: { id: string; name: string; eventType: string; playoffRound?: string | null; eventDate?: string | null; isComplete: boolean }) => ( + {events.map((event: { id: string; name: string; eventType: string; eventDate?: string | null; isComplete: boolean }) => (
@@ -167,7 +155,6 @@ export default function SportsSeasonEvents({ {getEventTypeLabel(event.eventType)} - {event.playoffRound && {event.playoffRound}} {event.eventDate && ( diff --git a/database/schema.ts b/database/schema.ts index dc28541..bada8e1 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -308,6 +308,9 @@ export const scoringEvents = pgTable("scoring_events", { playoffRound: varchar("playoff_round", { length: 50 }), // "Quarterfinals", "Semifinals", "Finals" // For qualifying events isQualifyingEvent: boolean("is_qualifying_event").notNull().default(false), + // Template system (Phase 2.6) + bracketTemplateId: varchar("bracket_template_id", { length: 50 }), // "ncaa_68", "nfl_14", "simple_16", etc. + scoringStartsAtRound: varchar("scoring_starts_at_round", { length: 50 }), // "Elite Eight", "Quarterfinals", etc. isComplete: boolean("is_complete").notNull().default(false), completedAt: timestamp("completed_at"), createdAt: timestamp("created_at").defaultNow().notNull(), @@ -351,6 +354,10 @@ export const playoffMatches = pgTable("playoff_matches", { // Optional detailed data participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }), participant2Score: decimal("participant2_score", { precision: 10, scale: 2 }), + // Template system (Phase 2.6) + isScoring: boolean("is_scoring").notNull().default(true), // Does this match affect fantasy points? + templateRound: varchar("template_round", { length: 50 }), // "First Four", "Round of 64", "Elite Eight", etc. + seedInfo: varchar("seed_info", { length: 50 }), // "1 vs 16", "11a vs 11b", etc. (for display) createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/drizzle/0023_cynical_jack_power.sql b/drizzle/0023_cynical_jack_power.sql new file mode 100644 index 0000000..858e07f --- /dev/null +++ b/drizzle/0023_cynical_jack_power.sql @@ -0,0 +1,5 @@ +ALTER TABLE "playoff_matches" ADD COLUMN "is_scoring" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "playoff_matches" ADD COLUMN "template_round" varchar(50);--> statement-breakpoint +ALTER TABLE "playoff_matches" ADD COLUMN "seed_info" varchar(50);--> statement-breakpoint +ALTER TABLE "scoring_events" ADD COLUMN "bracket_template_id" varchar(50);--> statement-breakpoint +ALTER TABLE "scoring_events" ADD COLUMN "scoring_starts_at_round" varchar(50); \ No newline at end of file diff --git a/drizzle/meta/0023_snapshot.json b/drizzle/meta/0023_snapshot.json new file mode 100644 index 0000000..ebd32e2 --- /dev/null +++ b/drizzle/meta/0023_snapshot.json @@ -0,0 +1,2730 @@ +{ + "id": "6fb2f02b-46e6-4683-a311-1907169289c1", + "prevId": "a01986c0-28e0-46a8-8a25-f599d06bcc00", + "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 + }, + "is_scoring": { + "name": "is_scoring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "template_round": { + "name": "template_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "seed_info": { + "name": "seed_info", + "type": "varchar(50)", + "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 + }, + "bracket_template_id": { + "name": "bracket_template_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "scoring_starts_at_round": { + "name": "scoring_starts_at_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": 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 9cc44ec..da97708 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1762191156317, "tag": "0022_shiny_mother_askani", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1762200550705, + "tag": "0023_cynical_jack_power", + "breakpoints": true } ] } \ No newline at end of file diff --git a/plans/scoring-system.md b/plans/scoring-system.md index 22fd967..2d7cd77 100644 --- a/plans/scoring-system.md +++ b/plans/scoring-system.md @@ -970,8 +970,8 @@ All clarification questions (Q1-Q21) have been answered and confirmed: - [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) ✅ *Completed* -**Goal**: Implement playoff bracket tracking and scoring for NFL/NBA/MLB +### Phase 2: Playoff Scoring (Single Elimination) +**Goal**: Implement playoff bracket tracking and scoring for all tournament types - [x] **2.1** Playoff event creation ✅ - [x] UI for creating playoff rounds (QF, SF, Finals) @@ -1012,6 +1012,132 @@ All clarification questions (Q1-Q21) have been answered and confirmed: - [x] Test tie scenarios (2-way, 4-way, edge cases) - [x] All tests passing ✅ +**LIMITATION: Phase 2.1-2.5 only supports 4 and 8 team brackets. Phases 2.6-2.9 expand to support all tournament sizes.** + +--- + +## Bracket Expansion (Phases 2.6-2.9) + +Current implementation only supports 4 and 8 team brackets. We need to support: +- **Standard brackets**: 4, 8, 16, 32 teams +- **March Madness**: 68 teams with First Four play-in games +- **NFL Playoffs**: 14 teams with bye weeks +- **NBA Playoffs**: 16 teams with conference structure +- **Scoring rounds vs non-scoring rounds**: March Madness only scores Elite Eight and beyond + +### Template-Based Bracket System + +Create pre-defined bracket templates for common structures, with flexible participant assignment. + +#### Bracket Template Structure + +```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? +} +``` + +#### Key Templates + +**NCAA March Madness (68 teams)** +- First Four (4 games, 8 teams) - play-in games, NOT scoring +- Round of 64 (32 games) - NOT scoring +- Round of 32 (16 games) - NOT scoring +- Sweet Sixteen (8 games) - NOT scoring +- Elite Eight (4 games) - **SCORING STARTS** (losers share 5th-8th) +- Final Four (2 games) - scoring (losers share 3rd-4th) +- Championship (1 game) - scoring (1st/2nd) + +**NFL Playoffs (14 teams)** +- Wild Card (6 games, 12 teams) - 2 teams get byes +- Divisional (4 games) - **SCORING STARTS** (QF, losers share 5th-8th) +- Conference Championship (2 games) - scoring (SF, losers share 3rd-4th) +- Super Bowl (1 game) - scoring (Finals, 1st/2nd) + +**NBA Playoffs (16 teams)** +- First Round (8 games) - NOT scoring +- Conference Semifinals (4 games) - **SCORING STARTS** (QF, losers share 5th-8th) +- Conference Finals (2 games) - scoring (SF, losers share 3rd-4th) +- NBA Finals (1 game) - scoring (Finals, 1st/2nd) + +**Simple 16-team and 32-team brackets** +- Start scoring at Quarterfinals (top 8) +- Earlier rounds award 0 points per Q20 + +#### 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", "Elite Eight", 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", "simple_16", etc. + scoringStartsAtRound: varchar(50) // Which round starts fantasy scoring +} +``` + +### Implementation Phases + +- [x] **2.6** Template System Foundation ✅ + - [x] Define bracket template TypeScript types and constants + - [x] Add `isScoring`, `templateRound`, `seedInfo` to playoff_matches schema + - [x] Add `bracketTemplateId`, `scoringStartsAtRound` to scoring_events schema + - [x] Create database migration (drizzle/0023) + - [x] Update playoff-match model to support templates + - [x] Create template definitions file with all bracket templates (app/lib/bracket-templates.ts) + - [x] Add `generateBracketFromTemplate()` function + - [x] Add `advanceWinnerTemplate()` function for template-based advancement + - [x] Test suite for template structures (16 tests passing) + +- [ ] **2.7** Simple Template Expansion (16, 32 teams) ⏭️ **NEXT** + - [x] Create template definitions for 16 and 32 team brackets (already done in 2.6) + - [ ] Update bracket generation UI to use template selector + - [ ] Test with 16-team bracket (Round of 16 → QF → SF → Finals) + - [ ] Test with 32-team bracket (Round of 32 → Round of 16 → QF → SF → Finals) + - [ ] Update scoring calculator for non-scoring rounds (0 points per Q20) + - [ ] Verify Elite Eight scoring starts correctly + +- [ ] **2.8** March Madness Template (68 teams) + - [ ] Create NCAA_68 template definition + - [ ] Build First Four matchup assignment UI (4 play-in games) + - [ ] Build Round of 64 with TBD slot handling (winners from First Four) + - [ ] Implement winner advancement from First Four to Round of 64 + - [ ] Test complete 68-team bracket flow + - [ ] Verify only Elite Eight and beyond score points + - [ ] Test early elimination = 0 points (per Q20) + +- [ ] **2.9** NFL/NBA Templates + - [ ] Create NFL_14 template (with bye weeks) + - [ ] Create NBA_16 template (with conference structure) + - [ ] Handle bye week logic (teams skip Wild Card round) + - [ ] Test NFL playoff bracket + - [ ] Test NBA playoff bracket + - [ ] Verify scoring starts at correct round for each template + +--- + ### Phase 3: Other Scoring Patterns **Goal**: Implement season standings (F1) and qualifying points (Golf/Tennis)