import type { Route } from "./+types/admin.sports-seasons.$id.events"; import { redirect } from "react-router"; import { logger } from "~/lib/logger"; import { findSportsSeasonById } from "~/models/sports-season"; import { getScoringEventsForSportsSeason, createScoringEvent, deleteScoringEvent, bulkCreateScoringEvents, ensurePrimaryEvent, countWindowsByTournament, getMajorsCompleted, type CreateScoringEventData, } from "~/models/scoring-event"; import { isBracketMajor } from "~/lib/event-utils"; import { getQPStandings } from "~/models/qualifying-points"; import { finalizeQualifyingPoints } from "~/models/scoring-calculator"; import { maybeResolveCompletedBracktForSportsSeason } from "~/services/brackt.server"; import { upsertTournament, findTournamentsBySport, getTournamentById } from "~/models/tournament"; import { extractTournamentIdentity } from "~/lib/tournament-identity"; 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 events = await getScoringEventsForSportsSeason(params.id); // For qualifying sports seasons, get QP standings with global ranks attached. // majorsCompleted is derived on read (count of completed qualifying events), not a // stored counter — see getMajorsCompleted. let qpStandings = null; const scoringRules = null; let majorsCompleted = 0; if (sportsSeason.scoringPattern === "qualifying_points") { majorsCompleted = await getMajorsCompleted(params.id); const standings = await getQPStandings(params.id); let prevQP = -1; let prevRankStart = 1; qpStandings = standings.map((s, index) => { const qp = parseFloat(s.totalQualifyingPoints); let rank: number; if (index > 0 && Math.abs(qp - prevQP) < 0.001) { rank = prevRankStart; } else { rank = index + 1; prevRankStart = rank; } prevQP = qp; return { ...s, globalRank: rank }; }); } // Tournaments for this sport that don't already have a scoring event in this season. // Only relevant for qualifying_points seasons — skip the query for all others. const allTournamentsForSport = sportsSeason.scoringPattern === "qualifying_points" ? await findTournamentsBySport(sportsSeason.sportId) : []; const linkedTournamentIds = new Set( events.map((e) => e.tournamentId).filter((id): id is string => id !== null) ); const availableTournaments = allTournamentsForSport.filter((t) => !linkedTournamentIds.has(t.id)); // Shared-tournament context for the delete confirmation dialog: for each event // linked to a canonical tournament, how many OTHER seasons ("windows") also use // it, and the tournament's name. Lets the UI explain exactly what a delete does. const sharedInfo = new Map(); await Promise.all( [...linkedTournamentIds].map(async (tid) => { const cached = allTournamentsForSport.find((t) => t.id === tid); const [tournament, windowCount] = await Promise.all([ cached ?? getTournamentById(tid), countWindowsByTournament(tid), ]); sharedInfo.set(tid, { name: tournament?.name ?? "shared tournament", windowCount, }); }) ); const eventsWithSharing = events.map((e) => { const info = e.tournamentId ? sharedInfo.get(e.tournamentId) : null; return { ...e, tournamentName: info?.name ?? null, otherWindowCount: info ? Math.max(0, info.windowCount - 1) : 0, }; }); return { sportsSeason: { ...sportsSeason, majorsCompleted } as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string }; }, events: eventsWithSharing, qpStandings, scoringRules, availableTournaments, }; } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "add-from-tournament") { const tournamentId = formData.get("tournamentId"); if (typeof tournamentId !== "string" || !tournamentId) { return { error: "Tournament ID is required" }; } try { const tournament = await getTournamentById(tournamentId); if (!tournament) return { error: "Tournament not found" }; const eventDate = tournament.startsAt ? new Date(tournament.startsAt) : undefined; // This action is only reachable for qualifying_points seasons (the card is // conditional in the UI), so major_tournament + isQualifyingEvent=true is always correct. const event = await createScoringEvent({ sportsSeasonId: params.id, name: `${tournament.name} ${tournament.year}`, eventType: "major_tournament", isQualifyingEvent: true, tournamentId: tournament.id, eventDate, eventStartsAt: tournament.startsAt ? new Date(tournament.startsAt) : undefined, }); // Bracket majors (tennis/CS2) need a primary window to be scorable. Seed it // on the first linked window; later windows stay siblings (idempotent). const ss = await findSportsSeasonById(params.id); if (isBracketMajor(ss?.sport?.simulatorType)) { await ensurePrimaryEvent(tournament.id, event.id); } return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`); } catch (error) { logger.error("Error adding scoring event from tournament:", error); return { error: "Failed to create scoring event from tournament." }; } } if (intent === "finalize-qp") { try { await finalizeQualifyingPoints(params.id); await maybeResolveCompletedBracktForSportsSeason(params.id); return { success: "Qualifying points finalized and fantasy placements assigned to top 8!" }; } catch (error) { logger.error("Error finalizing qualifying points:", error); return { error: error instanceof Error ? error.message : "Failed to finalize qualifying points" }; } } if (intent === "bulk-create") { const bulkText = formData.get("bulkEvents"); const bulkEventType = formData.get("bulkEventType"); if (typeof bulkText !== "string" || !bulkText.trim()) { return { error: "Event list is required" }; } const validTypes = ["playoff_game", "major_tournament", "schedule_event"] as const; type ValidEventType = typeof validTypes[number]; const eventType: ValidEventType = validTypes.includes(bulkEventType as ValidEventType) ? (bulkEventType as ValidEventType) : "playoff_game"; const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) return { error: "Sports season not found" }; const isQualifyingDefault = sportsSeason.scoringPattern === "qualifying_points" && eventType === "major_tournament"; // Parse lines: "Name, YYYY-MM-DD[, HH:MM]" or tab-separated, or just "Name" const lines = bulkText .split("\n") .map((l) => l.trim()) .filter(Boolean); const parsedEvents = lines.map((line) => { const parts = line.split(/[,\t]/).map((p) => p.trim()); const name = parts[0]; const dateStr = parts[1]; const timeStr = parts[2]; const eventDate = dateStr && /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? new Date(dateStr) : undefined; const eventStartsAt = eventDate && dateStr && timeStr && /^\d{2}:\d{2}$/.test(timeStr) ? new Date(`${dateStr}T${timeStr}:00.000Z`) : undefined; return { name, eventDate, eventStartsAt, eventType, isQualifyingEvent: isQualifyingDefault }; }); const validEvents = parsedEvents.filter((e) => e.name); if (validEvents.length === 0) { return { error: "No valid events found. Use format: Event Name, YYYY-MM-DD" }; } // Auto-provision canonical tournaments for qualifying events (check constraint). try { for (const ev of validEvents) { if (!ev.isQualifyingEvent) continue; const identity = extractTournamentIdentity({ name: ev.name, eventDate: ev.eventDate ? ev.eventDate.toISOString().slice(0, 10) : null, eventType: ev.eventType, }); const tournament = await upsertTournament({ sportId: sportsSeason.sportId, name: identity.name, year: identity.year, startsAt: ev.eventStartsAt ?? null, }); (ev as typeof ev & { tournamentId: string }).tournamentId = tournament.id; } } catch (err) { logger.error("Error linking qualifying events to canonical tournaments:", err); return { error: "Could not determine tournament identity for one or more qualifying events. Include a 4-digit year or date for each.", }; } try { const created = await bulkCreateScoringEvents(params.id, validEvents); // Seed a primary window per bracket-major tournament that lacks one. if (isBracketMajor(sportsSeason.sport?.simulatorType)) { for (const ev of created) { if (ev.tournamentId) await ensurePrimaryEvent(ev.tournamentId, ev.id); } } return { success: `Created ${validEvents.length} event${validEvents.length !== 1 ? "s" : ""} successfully` }; } catch (error) { logger.error("Error bulk creating events:", error); return { error: "Failed to create events. Please try again." }; } } if (intent === "delete-event") { const eventId = formData.get("eventId"); if (typeof eventId !== "string" || !eventId) { return { error: "Event ID is required" }; } try { const result = await deleteScoringEvent(eventId, undefined, { deleteOrphanTournament: formData.get("deleteTournament") === "1", }); let success = "Event deleted"; if (result.deletedTournament) { success = "Event deleted and its shared tournament removed"; } else if (result.tournamentId && result.remainingWindows > 0) { success = `Event removed from this season (shared tournament kept — still used by ${result.remainingWindows} season${result.remainingWindows !== 1 ? "s" : ""})`; if (result.promotedPrimaryId) { success += "; another season was promoted to primary"; } } return { success }; } catch (error) { logger.error("Error deleting event:", error); return { error: "Failed to delete event" }; } } const name = formData.get("name"); const eventType = formData.get("eventType"); const eventStartsAtRaw = formData.get("eventStartsAt"); // Validation if (typeof name !== "string" || !name.trim()) { return { error: "Event name is required" }; } if ( eventType !== "playoff_game" && eventType !== "major_tournament" && eventType !== "schedule_event" ) { return { error: "Invalid event type" }; } // Get the sports season to check if this should be a qualifying event const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) { return { error: "Sports season not found" }; } // Automatically mark major tournaments as qualifying events for qualifying_points sports seasons const isQualifyingEvent = sportsSeason.scoringPattern === "qualifying_points" && eventType === "major_tournament"; // eventStartsAt is a UTC ISO string produced by localDateTimeToUtcIso on the client const eventStartsAt = typeof eventStartsAtRaw === "string" && eventStartsAtRaw ? new Date(eventStartsAtRaw) : undefined; // Derive eventDate from eventStartsAt (UTC date portion) const eventDate = eventStartsAt ? new Date(eventStartsAt.toISOString().split("T")[0]) : undefined; const eventData: CreateScoringEventData = { sportsSeasonId: params.id, name: name.trim(), eventType: eventType as "playoff_game" | "major_tournament" | "schedule_event", eventDate, eventStartsAt, isQualifyingEvent, }; // Qualifying events require a canonical tournament_id (check constraint). // Auto-provision one by extracting (name, year) from the event name/date. if (isQualifyingEvent) { try { const identity = extractTournamentIdentity({ name: eventData.name, eventDate: eventDate ? eventDate.toISOString().slice(0, 10) : null, eventType, }); const tournament = await upsertTournament({ sportId: sportsSeason.sportId, name: identity.name, year: identity.year, startsAt: eventStartsAt ?? null, }); eventData.tournamentId = tournament.id; } catch (err) { logger.error("Error linking qualifying event to canonical tournament:", err); return { error: "Could not determine tournament identity. Include a 4-digit year in the event name, or set a date.", }; } } try { const event = await createScoringEvent(eventData); // Bracket majors need a primary window to be scorable — seed it on the first // linked window (idempotent for later windows). if (eventData.tournamentId && isBracketMajor(sportsSeason.sport?.simulatorType)) { await ensurePrimaryEvent(eventData.tournamentId, event.id); } return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`); } catch (error) { logger.error("Error creating scoring event:", error); return { error: "Failed to create scoring event. Please try again." }; } }