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"; 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); 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 // 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( "~/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" }; }