brackt/app/routes/admin.sports-seasons.$id.events.server.ts
chrisp 58e656f15e
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m14s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
claude/wimbledon-qp-scoring-bug-xevhlf (#127)
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #127
2026-07-03 21:40:11 +00:00

353 lines
13 KiB
TypeScript

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<string, { name: string; windowCount: number }>();
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." };
}
}