brackt/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts
Chris Parsons a3b8fa6309 feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00

255 lines
8.1 KiB
TypeScript

import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
import { redirect } from "react-router";
import { findSportsSeasonById } from "~/models/sports-season";
import { findParticipantsBySportsSeasonId } from "~/models/participant";
import { getScoringEventById } from "~/models/scoring-event";
import {
findPlayoffMatchesByEventId,
generateSingleEliminationBracket,
setMatchWinner,
advanceWinner,
findPlayoffMatchById,
} from "~/models/playoff-match";
import { processPlayoffEvent } from "~/models/scoring-calculator";
export async function loader({ params }: Route.LoaderArgs) {
const sportsSeason = await findSportsSeasonById(params.id);
if (!sportsSeason) {
throw new Response("Sports season not found", { status: 404 });
}
const event = await getScoringEventById(params.eventId);
if (!event) {
throw new Response("Event not found", { status: 404 });
}
if (event.eventType !== "playoff_game") {
throw new Response("This event is not a playoff event", { status: 400 });
}
const participants = await findParticipantsBySportsSeasonId(params.id);
const matches = await findPlayoffMatchesByEventId(params.eventId);
// The matches already include participant relations from the model query
return {
sportsSeason: sportsSeason as typeof sportsSeason & {
sport: { id: string; name: string; type: string; slug: string };
},
event,
participants,
matches: matches as Array<typeof matches[0] & {
participant1: { id: string; name: string } | null;
participant2: { id: string; name: string } | null;
winner: { id: string; name: string } | null;
loser: { id: string; name: string } | null;
}>,
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "generate-bracket") {
const bracketSize = formData.get("bracketSize");
if (bracketSize !== "4" && bracketSize !== "8") {
return { error: "Invalid bracket size" };
}
const size = parseInt(bracketSize, 10);
const participantIds: string[] = [];
for (let i = 0; i < size; i++) {
const participantId = formData.get(`participant${i}`);
if (typeof participantId !== "string" || !participantId) {
return { error: `Participant ${i + 1} is required` };
}
participantIds.push(participantId);
}
// Check for duplicates
const uniqueParticipants = new Set(participantIds);
if (uniqueParticipants.size !== participantIds.length) {
return { error: "Each participant can only be selected once" };
}
try {
await generateSingleEliminationBracket(params.eventId, participantIds);
return { success: "Bracket generated successfully" };
} catch (error) {
console.error("Error generating bracket:", error);
return {
error:
error instanceof Error
? error.message
: "Failed to generate bracket",
};
}
}
if (intent === "set-winner") {
const matchId = formData.get("matchId");
const winnerId = formData.get("winnerId");
if (typeof matchId !== "string" || !matchId) {
return { error: "Match ID is required" };
}
if (typeof winnerId !== "string" || !winnerId) {
return { error: "Winner ID is required" };
}
try {
const match = await findPlayoffMatchById(matchId);
if (!match) {
return { error: "Match not found" };
}
// Determine loser
const loserId =
match.participant1Id === winnerId
? match.participant2Id
: match.participant1Id;
if (!loserId) {
return { error: "Could not determine loser" };
}
// Set the winner
await setMatchWinner(matchId, winnerId, loserId);
// Try to advance the winner to the next round (if not Finals)
if (match.round !== "Finals") {
try {
await advanceWinner(matchId, winnerId);
} catch (error) {
// Only ignore "already filled" errors, re-throw unexpected errors
if (
error instanceof Error &&
error.message.includes("already filled")
) {
console.warn("Match already filled, skipping advancement");
} else {
throw error; // Re-throw unexpected errors
}
}
}
return { success: "Winner set successfully" };
} catch (error) {
console.error("Error setting winner:", error);
return {
error:
error instanceof Error ? error.message : "Failed to set winner",
};
}
}
if (intent === "complete-round") {
const round = formData.get("round");
if (
round !== "Quarterfinals" &&
round !== "Semifinals" &&
round !== "Finals"
) {
return { error: "Invalid round" };
}
try {
// Get the event and update playoffRound to the completed round
const event = await getScoringEventById(params.eventId);
if (!event) {
return { error: "Event not found" };
}
// Verify all matches in this round are complete
const matches = await findPlayoffMatchesByEventId(params.eventId);
const roundMatches = matches.filter((m) => m.round === round);
const allComplete = roundMatches.every((m) => m.isComplete);
if (!allComplete) {
return { error: `Not all matches in ${round} are complete` };
}
// Validate round order: ensure previous rounds are complete
const { findParticipantResultsBySportsSeasonId } = await import(
"~/models/participant-result"
);
const existingResults = await findParticipantResultsBySportsSeasonId(
params.id
);
if (round === "Finals") {
// Finals requires Semifinals to be complete (3rd/4th place results exist)
const hasSemifinalResults = existingResults.some(
(r) => r.finalPosition === 3 || r.finalPosition === 4
);
if (!hasSemifinalResults) {
return {
error:
"Semifinals must be completed before Finals (no 3rd/4th place results found)",
};
}
} else if (round === "Semifinals") {
// Semifinals requires Quarterfinals to be complete if QF matches exist
const hasQuarterfinals = matches.some((m) => m.round === "Quarterfinals");
if (hasQuarterfinals) {
const hasQuarterfinalsResults = existingResults.some(
(r) =>
r.finalPosition === 5 ||
r.finalPosition === 6 ||
r.finalPosition === 7 ||
r.finalPosition === 8
);
if (!hasQuarterfinalsResults) {
return {
error:
"Quarterfinals must be completed before Semifinals (no 5th-8th place results found)",
};
}
}
}
// Get the sports season to find a fantasy season for scoring rules
const sportsSeason = await findSportsSeasonById(params.id);
if (!sportsSeason) {
return { error: "Sports season not found" };
}
// Process the playoff event to calculate placements
// We need to temporarily update the event's playoffRound to the one being completed
const { updateScoringEvent } = await import("~/models/scoring-event");
await updateScoringEvent(params.eventId, { playoffRound: round });
// Get a fantasy season ID for scoring calculation
const { findSeasonSportsBySportsSeasonId } = await import(
"~/models/season-sport"
);
const seasonSports = await findSeasonSportsBySportsSeasonId(params.id);
if (seasonSports.length === 0) {
return {
error: "No fantasy seasons found for this sports season",
};
}
// Process playoff event to update participant results
await processPlayoffEvent(params.eventId);
return {
success: `${round} completed and placements calculated successfully`,
};
} catch (error) {
console.error("Error completing round:", error);
return {
error:
error instanceof Error ? error.message : "Failed to complete round",
};
}
}
return { error: "Invalid action" };
}