brackt/app/routes/admin.sports-seasons.$id.events.server.ts

188 lines
6.3 KiB
TypeScript
Raw Normal View History

import type { Route } from "./+types/admin.sports-seasons.$id.events";
import { redirect } from "react-router";
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";
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
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;
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
const scoringRules = null;
if (sportsSeason.scoringPattern === "qualifying_points") {
qpStandings = await getQPStandings(params.id);
// Get scoring rules from a linked season (if any)
// For now, we'll use default scoring for projection
// Note: QP standings are global to the sports season, not league-specific
// When showing to league members, we would filter by their league's scoring rules
}
return {
sportsSeason: sportsSeason as typeof sportsSeason & {
sport: { id: string; name: string; type: string; slug: string };
},
events,
qpStandings,
scoringRules,
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "finalize-qp") {
try {
await finalizeQualifyingPoints(params.id);
return { success: "Qualifying points finalized and fantasy placements assigned to top 8!" };
} catch (error) {
console.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" };
}
try {
await bulkCreateScoringEvents(params.id, validEvents);
return { success: `Created ${validEvents.length} event${validEvents.length !== 1 ? "s" : ""} successfully` };
} catch (error) {
console.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) {
console.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,
};
try {
const event = await createScoringEvent(eventData);
return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`);
} catch (error) {
console.error("Error creating scoring event:", error);
return { error: "Failed to create scoring event. Please try again." };
}
}