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, updateScoringEvent } from "~/models/scoring-event"; import { findPlayoffMatchesByEventId, generateBracketFromTemplate, setMatchWinner, advanceWinnerTemplate, findPlayoffMatchById, assignParticipantsToKnockout, } from "~/models/playoff-match"; import { createGame, updateGame, deleteGame, type PlayoffMatchGameStatus, } from "~/models/playoff-match-game"; import { upsertMatchOdds, deleteOddsForParticipant, } from "~/models/playoff-match-odds"; import { processPlayoffEvent, processMatchResult, recalculateAffectedLeagues, recalculateStandings, } from "~/models/scoring-calculator"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; import { setParticipantResult, findParticipantResultsBySportsSeasonId } from "~/models/participant-result"; import { findSeasonSportsBySportsSeasonId } from "~/models/season-sport"; import { createDailySnapshot } from "~/models/standings"; import { createGroupsForEvent, addMembersToGroup, findGroupsByEventId, toggleMemberEliminated, getAdvancingParticipantIds, getEliminatedParticipantIds, } from "~/models/tournament-group"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and } 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); // Fetch tournament groups if this event has a group-stage template const tournamentGroups = await findGroupsByEventId(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, tournamentGroups, }; } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "generate-bracket") { const templateId = formData.get("templateId"); if (typeof templateId !== "string" || !templateId) { return { error: "Template ID is required" }; } const template = getBracketTemplate(templateId); if (!template) { return { error: "Invalid bracket template" }; } // Parse per-event region config for NCAA-style brackets let regionOverride: BracketRegion[] | undefined; if (template.regions && template.regions.length > 0) { const regions: BracketRegion[] = []; for (let r = 0; r < template.regions.length; r++) { const name = (formData.get(`regionName${r}`) as string | null)?.trim() || `Region ${r + 1}`; const seedsRaw = (formData.get(`regionPlayInSeeds${r}`) as string | null) || ""; const playInSeeds = seedsRaw .split(",") .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n) && n >= 1 && n <= 16); const playInSet = new Set(playInSeeds); regions.push({ name, directSeeds: ALL_16_SEEDS.filter((s) => !playInSet.has(s)), playIns: playInSeeds.map((s) => ({ seedSlot: s, teams: 2 as const })), }); } // Validate total = template.totalTeams const total = regions.reduce( (sum, r) => sum + r.directSeeds.length + r.playIns.length * 2, 0 ); if (total !== template.totalTeams) { return { error: `Region config accounts for ${total} team slots but template requires ${template.totalTeams}`, }; } regionOverride = regions; } const participantIds: string[] = []; for (let i = 0; i < template.totalTeams; 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 generateBracketFromTemplate(params.eventId, templateId, participantIds, regionOverride); // PHASE 5.3: Mark participants NOT in the bracket as eliminated const event = await getScoringEventById(params.eventId); if (event) { // Get all participants in the sports season const allParticipants = await findParticipantsBySportsSeasonId(params.id); // Create a set of participants in the bracket for fast lookup const participantsInBracket = new Set(participantIds); // Mark participants NOT in the bracket as eliminated for (const participant of allParticipants) { if (!participantsInBracket.has(participant.id)) { await setParticipantResult( participant.id, event.sportsSeasonId, 0 // 0 = didn't make playoffs, eliminated ); } } console.log(`[BracketGeneration] Marked ${allParticipants.length - participantIds.length} participants as eliminated`); } // Update the event to store the template ID, scoring start round, and region config await updateScoringEvent(params.eventId, { bracketTemplateId: templateId, scoringStartsAtRound: template.scoringStartsAtRound, bracketRegionConfig: regionOverride, }); return { success: "Bracket generated successfully" }; } catch (error) { console.error("Error generating bracket:", error); return { error: error instanceof Error ? error.message : "Failed to generate bracket", }; } } /** * Auto-complete a round when every match in it is marked complete. * Updates scoringEvents.playoffRound and calls processPlayoffEvent so that * non-bracket eliminations are recorded and probabilities are refreshed. * This replaces the manual "Complete Round" button. * * skipRecalculate=true because the caller already sent a Discord notification * scoped to the current batch of matches — we don't want a second one that * would show all previously-completed matches in the event. */ async function autoCompleteRoundIfDone( eventId: string, round: string, sportsSeasonId: string, db: ReturnType ): Promise { const allMatches = await findPlayoffMatchesByEventId(eventId); const roundMatches = allMatches.filter((m) => m.round === round); if (roundMatches.length === 0) return; const allDone = roundMatches.every((m) => m.isComplete); if (!allDone) return; await db .update(schema.scoringEvents) .set({ playoffRound: round, updatedAt: new Date() }) .where(eq(schema.scoringEvents.id, eventId)); await processPlayoffEvent(eventId, db, { skipRecalculate: true }); console.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`); } 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" }; } // Get the event to determine the template const event = await getScoringEventById(match.scoringEventId); if (!event) { return { error: "Event 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 using template if (event.bracketTemplateId) { const template = getBracketTemplate(event.bracketTemplateId); if (template) { try { await advanceWinnerTemplate(matchId, winnerId, template); } 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 } } } } // Immediately score this match: loser gets their final placement, // winner gets provisional floor points (isPartialScore=true). // isScoring ?? true: default to true for legacy rows pre-dating the isScoring column. await processMatchResult({ round: match.round, winnerId, loserId, isScoring: match.isScoring ?? true, sportsSeasonId: event.sportsSeasonId, bracketTemplateId: event.bracketTemplateId, eventId: event.id, eventName: event.name ?? undefined, matchId, }); const db = database(); await autoCompleteRoundIfDone(event.id, match.round, event.sportsSeasonId, db); 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 === "set-round-winners") { const round = formData.get("round"); if (typeof round !== "string" || !round) { return { error: "Round is required" }; } try { // Get all winner assignments from form data const winnerAssignments: Array<{ matchId: string; winnerId: string }> = []; for (const [key, value] of formData.entries()) { if (key.startsWith("winner-") && typeof value === "string") { const matchId = key.replace("winner-", ""); winnerAssignments.push({ matchId, winnerId: value }); } } if (winnerAssignments.length === 0) { return { error: "No winners selected" }; } // Get the event to determine the template const event = await getScoringEventById(params.eventId); if (!event) { return { error: "Event not found" }; } const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null; // Process each winner assignment let successCount = 0; const errors: string[] = []; const processedMatchIds: string[] = []; for (const { matchId, winnerId } of winnerAssignments) { try { const match = await findPlayoffMatchById(matchId); if (!match) { errors.push(`Match ${matchId} not found`); continue; } // Guard: ensure the match actually belongs to the submitted round. if (match.round !== round) { errors.push(`Match ${match.matchNumber} is in round "${match.round}", not "${round}"`); continue; } // Determine loser const loserId = match.participant1Id === winnerId ? match.participant2Id : match.participant1Id; if (!loserId) { errors.push(`Could not determine loser for match ${match.matchNumber}`); continue; } // Set the winner await setMatchWinner(matchId, winnerId, loserId); // Try to advance the winner to the next round using template if (template) { try { await advanceWinnerTemplate(matchId, winnerId, template); } catch (error) { // Only ignore "already filled" errors if ( error instanceof Error && error.message.includes("already filled") ) { console.warn("Match already filled, skipping advancement"); } else { throw error; } } } // Score this match without triggering standings/probability recalc yet — // we batch those side effects into a single call after the loop. // isScoring ?? true: default to true for legacy rows pre-dating the isScoring column. await processMatchResult({ round: match.round, winnerId, loserId, isScoring: match.isScoring ?? true, sportsSeasonId: event.sportsSeasonId, bracketTemplateId: event.bracketTemplateId, eventName: event.name ?? undefined, skipSideEffects: true, }); successCount++; processedMatchIds.push(matchId); } catch (error) { console.error(`Error setting winner for match ${matchId}:`, error); errors.push( `Match ${matchId}: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // Run side effects once for the whole batch (avoids N redundant recalcs). // Pass matchIds so Discord only shows the matches from this submission, not all // previously completed matches in the event. if (successCount > 0) { const db = database(); await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined, matchIds: processedMatchIds }); try { await updateProbabilitiesAfterResult(event.sportsSeasonId, true); } catch (error) { console.error(`Error updating probabilities after batch round winners:`, error); } await autoCompleteRoundIfDone(event.id, round, event.sportsSeasonId, db); } if (errors.length > 0 && successCount === 0) { return { error: `Failed to set winners: ${errors.join(", ")}` }; } if (errors.length > 0) { return { success: `Set ${successCount} winner(s) successfully`, error: `Some errors occurred: ${errors.join(", ")}`, }; } return { success: `Successfully set ${successCount} winner(s) for ${round}` }; } catch (error) { console.error("Error setting round winners:", error); return { error: error instanceof Error ? error.message : "Failed to set winners", }; } } if (intent === "complete-round") { const round = formData.get("round"); if (typeof round !== "string" || !round) { return { error: "Round is required" }; } 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); // Validate that the round exists in this event's matches const roundMatches = matches.filter((m) => m.round === round); if (roundMatches.length === 0) { return { error: `Round "${round}" not found in this bracket` }; } 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 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 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", }; } } if (intent === "reprocess-eliminations") { try { // Get the event const event = await getScoringEventById(params.eventId); if (!event) { return { error: "Event not found" }; } // Get all matches in the bracket const matches = await findPlayoffMatchesByEventId(params.eventId); if (matches.length === 0) { return { error: "No bracket exists for this event" }; } // Get all participants in the sports season const allParticipants = await findParticipantsBySportsSeasonId(params.id); // Collect all participant IDs that appear in ANY match const participantsInBracket = new Set(); for (const match of matches) { if (match.participant1Id) participantsInBracket.add(match.participant1Id); if (match.participant2Id) participantsInBracket.add(match.participant2Id); } // Mark participants NOT in the bracket as eliminated let eliminatedCount = 0; for (const participant of allParticipants) { if (!participantsInBracket.has(participant.id)) { await setParticipantResult( participant.id, event.sportsSeasonId, 0 // 0 = didn't make playoffs, eliminated ); eliminatedCount++; } } console.log(`[ReprocessEliminations] Marked ${eliminatedCount} participants as eliminated`); return { success: `Successfully marked ${eliminatedCount} participant(s) as eliminated`, }; } catch (error) { console.error("Error reprocessing eliminations:", error); return { error: error instanceof Error ? error.message : "Failed to reprocess eliminations", }; } } if (intent === "finalize-bracket") { try { // Get the event const event = await getScoringEventById(params.eventId); if (!event) { return { error: "Event not found" }; } // Get all matches const matches = await findPlayoffMatchesByEventId(params.eventId); if (matches.length === 0) { return { error: "No bracket exists for this event" }; } // Get template to determine round order const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null; if (!template) { return { error: "Bracket template not found" }; } // Verify ALL matches are complete const incompleteMatches = matches.filter((m) => !m.isComplete); if (incompleteMatches.length > 0) { return { error: `Cannot finalize: ${incompleteMatches.length} match(es) still incomplete`, }; } // Get all participants in this sports season const allParticipants = await findParticipantsBySportsSeasonId(params.id); // Get participants in matches const participantsInMatches = new Set( matches.flatMap((m) => [m.participant1Id, m.participant2Id].filter(Boolean)) ); // Process all rounds in order const db = database(); for (const round of template.rounds) { const roundMatches = matches.filter((m) => m.round === round.name); if (roundMatches.length === 0) continue; // Set playoffRound and process this round await db .update(schema.scoringEvents) .set({ playoffRound: round.name, updatedAt: new Date() }) .where(eq(schema.scoringEvents.id, params.eventId)); await processPlayoffEvent(params.eventId, db); } // Assign 0 points to participants not in the bracket (Q20) for (const participant of allParticipants) { if (!participantsInMatches.has(participant.id)) { await setParticipantResult( participant.id, params.id, 0 // 0 placement = 0 points ); } } // Mark event as complete await db .update(schema.scoringEvents) .set({ isComplete: true, completedAt: new Date(), updatedAt: new Date() }) .where(eq(schema.scoringEvents.id, params.eventId)); // Recalculate standings for all affected fantasy seasons const seasonSports = await findSeasonSportsBySportsSeasonId(params.id); for (const seasonSport of seasonSports) { await recalculateStandings(seasonSport.seasonId, db); await createDailySnapshot(seasonSport.seasonId, db); } return { success: `Bracket finalized! All placements calculated and standings updated.`, }; } catch (error) { console.error("Error finalizing bracket:", error); return { error: error instanceof Error ? error.message : "Failed to finalize bracket", }; } } if (intent === "generate-groups") { const templateId = formData.get("templateId"); if (typeof templateId !== "string" || !templateId) { return { error: "Template ID is required" }; } const template = getBracketTemplate(templateId); if (!template || !template.groupStage) { return { error: "Invalid template or template has no group stage" }; } const { groupStage } = template; const totalTeams = groupStage.groupCount * groupStage.teamsPerGroup; // Collect participant IDs from form const participantIds: string[] = []; for (let i = 0; i < totalTeams; 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 { // Update the event with template info await updateScoringEvent(params.eventId, { bracketTemplateId: templateId, scoringStartsAtRound: template.scoringStartsAtRound, }); // Create tournament groups const groups = await createGroupsForEvent(params.eventId, groupStage.groupLabels); // Distribute participants into groups (in selection order) for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { const startIdx = groupIndex * groupStage.teamsPerGroup; const groupParticipantIds = participantIds.slice( startIdx, startIdx + groupStage.teamsPerGroup ); await addMembersToGroup(groups[groupIndex].id, groupParticipantIds); } // Generate the empty knockout bracket structure await generateBracketFromTemplate(params.eventId, templateId); return { success: "Groups and knockout bracket structure created successfully" }; } catch (error) { console.error("Error generating groups:", error); return { error: error instanceof Error ? error.message : "Failed to generate groups", }; } } if (intent === "toggle-elimination") { const memberId = formData.get("memberId"); if (typeof memberId !== "string" || !memberId) { return { error: "Member ID is required" }; } try { const updated = await toggleMemberEliminated(memberId); return { success: updated.eliminated ? "Team marked as eliminated" : "Team reinstated", }; } catch (error) { console.error("Error toggling elimination:", error); return { error: error instanceof Error ? error.message : "Failed to toggle elimination", }; } } if (intent === "populate-knockout") { try { const event = await getScoringEventById(params.eventId); if (!event) { return { error: "Event not found" }; } // Parse match assignments from form const assignments: Array<{ matchNumber: number; slot: "participant1Id" | "participant2Id"; participantId: string; }> = []; for (const [key, value] of formData.entries()) { const matchPattern = /^match-(\d+)-(participant1Id|participant2Id)$/; const match = key.match(matchPattern); if (match && typeof value === "string" && value) { assignments.push({ matchNumber: parseInt(match[1], 10), slot: match[2] as "participant1Id" | "participant2Id", participantId: value, }); } } if (assignments.length !== 32) { return { error: `Expected 32 assignments but got ${assignments.length}` }; } // Validate all assignments are unique participants const assignedParticipantIds = new Set(assignments.map((a) => a.participantId)); if (assignedParticipantIds.size !== 32) { return { error: "All 32 knockout slots must have unique participants" }; } // Validate all assigned participants are non-eliminated const advancingIds = new Set(await getAdvancingParticipantIds(params.eventId)); for (const participantId of assignedParticipantIds) { if (!advancingIds.has(participantId)) { return { error: "All assigned participants must be non-eliminated group members" }; } } // Assign participants to knockout bracket await assignParticipantsToKnockout(params.eventId, assignments); // Mark eliminated group participants with finalPosition = 0 const eliminatedIds = await getEliminatedParticipantIds(params.eventId); for (const participantId of eliminatedIds) { await setParticipantResult(participantId, event.sportsSeasonId, 0); } console.log( `[PopulateKnockout] Assigned 32 participants to knockout, marked ${eliminatedIds.length} as eliminated` ); return { success: "Knockout bracket populated successfully" }; } catch (error) { console.error("Error populating knockout:", error); return { error: error instanceof Error ? error.message : "Failed to populate knockout", }; } } // ── Game scheduling ────────────────────────────────────────────────────── if (intent === "add-game") { const matchId = formData.get("matchId"); const gameNumber = formData.get("gameNumber"); const scheduledAt = formData.get("scheduledAt"); const notes = formData.get("notes"); if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" }; if (typeof gameNumber !== "string" || !gameNumber) return { error: "gameNumber is required" }; try { await createGame({ playoffMatchId: matchId, gameNumber: parseInt(gameNumber, 10), scheduledAt: scheduledAt ? new Date(scheduledAt as string) : null, notes: notes ? (notes as string) : null, }); return { success: "Game added" }; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to add game" }; } } if (intent === "update-game") { const gameId = formData.get("gameId"); const scheduledAt = formData.get("scheduledAt"); const status = formData.get("status"); const participant1Score = formData.get("participant1Score"); const participant2Score = formData.get("participant2Score"); const winnerId = formData.get("winnerId"); const notes = formData.get("notes"); if (typeof gameId !== "string" || !gameId) return { error: "gameId is required" }; const validStatuses: PlayoffMatchGameStatus[] = ["scheduled", "complete", "postponed"]; if (status && !validStatuses.includes(status as PlayoffMatchGameStatus)) { return { error: `Invalid status: must be one of ${validStatuses.join(", ")}` }; } try { const updated = await updateGame(gameId, { ...(scheduledAt !== null && { scheduledAt: scheduledAt ? new Date(scheduledAt as string) : null }), ...(status && { status: status as PlayoffMatchGameStatus }), ...(participant1Score !== null && { participant1Score: (participant1Score as string) || null }), ...(participant2Score !== null && { participant2Score: (participant2Score as string) || null }), ...(winnerId !== null && { winnerId: (winnerId as string) || null }), ...(notes !== null && { notes: (notes as string) || null }), }); if (!updated) return { error: "Game not found" }; return { success: "Game updated" }; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to update game" }; } } if (intent === "delete-game") { const gameId = formData.get("gameId"); if (typeof gameId !== "string" || !gameId) return { error: "gameId is required" }; try { await deleteGame(gameId); return { success: "Game deleted" }; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to delete game" }; } } // ── Odds management ────────────────────────────────────────────────────── if (intent === "upsert-odds") { const matchId = formData.get("matchId"); const participantId = formData.get("participantId"); const moneylineOdds = formData.get("moneylineOdds"); const oddsSource = formData.get("oddsSource"); if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" }; if (typeof participantId !== "string" || !participantId) return { error: "participantId is required" }; if (typeof moneylineOdds !== "string" || !moneylineOdds) return { error: "moneylineOdds is required" }; const moneylineInt = parseInt(moneylineOdds, 10); if (isNaN(moneylineInt)) return { error: "moneylineOdds must be an integer" }; try { await upsertMatchOdds(matchId, participantId, { moneylineOdds: moneylineInt, oddsSource: oddsSource ? (oddsSource as string) : undefined, }); return { success: "Odds updated" }; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to update odds" }; } } if (intent === "delete-odds") { const matchId = formData.get("matchId"); const participantId = formData.get("participantId"); if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" }; if (typeof participantId !== "string" || !participantId) return { error: "participantId is required" }; try { await deleteOddsForParticipant(matchId, participantId); return { success: "Odds deleted" }; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to delete odds" }; } } return { error: "Invalid action" }; }