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, type CreateScoringEventData, } from "~/models/scoring-event"; import { getQPStandings } from "~/models/qualifying-points"; import { finalizeQualifyingPoints } from "~/models/scoring-calculator"; 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 let qpStandings = null; const scoringRules = null; if (sportsSeason.scoringPattern === "qualifying_points") { qpStandings = await getQPStandings(params.id); } // 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)); return { sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string }; }, events, 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, }); 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); 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 { await bulkCreateScoringEvents(params.id, validEvents); 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 { await deleteScoringEvent(eventId); return { success: "Event deleted successfully" }; } 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); 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." }; } }