2026-03-21 00:12:01 -07:00
|
|
|
|
import { Form, Link, redirect, useNavigate, useNavigation } from "react-router";
|
2026-03-17 14:34:09 -07:00
|
|
|
|
import { getAuth } from "@clerk/react-router/server";
|
|
|
|
|
|
import { isUserAdminByClerkId } from "~/models/user";
|
2025-10-12 21:54:49 -07:00
|
|
|
|
import type { Route } from "./+types/admin.sports-seasons.$id";
|
2026-03-10 12:10:52 -07:00
|
|
|
|
|
2026-03-21 13:41:39 -07:00
|
|
|
|
import { logger } from "~/lib/logger";
|
2026-03-21 09:44:05 -07:00
|
|
|
|
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason, type NewSportsSeason } from "~/models/sports-season";
|
2026-03-17 14:34:09 -07:00
|
|
|
|
import { processSeasonStandings, recalculateStandings } from "~/models/scoring-calculator";
|
2026-03-18 22:15:28 -07:00
|
|
|
|
import { createDailySnapshot } from "~/models/standings";
|
2026-03-09 15:34:31 -07:00
|
|
|
|
import { database } from "~/database/context";
|
2026-03-17 14:34:09 -07:00
|
|
|
|
import { participantEvSnapshots, seasonSports } from "~/database/schema";
|
2026-03-09 15:34:31 -07:00
|
|
|
|
import { eq, desc } from "drizzle-orm";
|
|
|
|
|
|
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
2026-03-21 00:12:01 -07:00
|
|
|
|
import { syncStandings } from "~/services/standings-sync/index";
|
|
|
|
|
|
import {
|
|
|
|
|
|
getPendingStandingsMappings,
|
|
|
|
|
|
deletePendingStandingsMapping,
|
|
|
|
|
|
} from "~/models/pending-standings-mappings";
|
2026-03-21 09:44:05 -07:00
|
|
|
|
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant";
|
|
|
|
|
|
import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
2025-10-12 21:54:49 -07:00
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
|
import { Input } from "~/components/ui/input";
|
|
|
|
|
|
import { Label } from "~/components/ui/label";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
CardContent,
|
|
|
|
|
|
CardDescription,
|
|
|
|
|
|
CardHeader,
|
|
|
|
|
|
CardTitle,
|
|
|
|
|
|
} from "~/components/ui/card";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "~/components/ui/select";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AlertDialog,
|
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
|
} from "~/components/ui/alert-dialog";
|
2026-03-07 21:59:29 -08:00
|
|
|
|
import { Badge } from "~/components/ui/badge";
|
2026-03-18 22:40:19 -07:00
|
|
|
|
import { Checkbox } from "~/components/ui/checkbox";
|
2026-03-21 00:12:01 -07:00
|
|
|
|
import { Trash2, Users, Trophy, Calculator, CheckCircle2, Zap, AlertTriangle, Loader2, RefreshCw } from "lucide-react";
|
2025-11-11 10:08:25 -08:00
|
|
|
|
import { useState } from "react";
|
2025-10-12 21:54:49 -07:00
|
|
|
|
|
2026-03-10 12:10:52 -07:00
|
|
|
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|
|
|
|
|
return [{ title: `${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:54:49 -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 participants = await findParticipantsBySportsSeasonId(params.id);
|
|
|
|
|
|
|
2026-03-09 15:34:31 -07:00
|
|
|
|
// Get the most recent snapshot date (if any)
|
|
|
|
|
|
const db = database();
|
|
|
|
|
|
const lastSnapshot = await db
|
|
|
|
|
|
.select({ snapshotDate: participantEvSnapshots.snapshotDate })
|
|
|
|
|
|
.from(participantEvSnapshots)
|
|
|
|
|
|
.where(eq(participantEvSnapshots.sportsSeasonId, params.id))
|
|
|
|
|
|
.orderBy(desc(participantEvSnapshots.snapshotDate))
|
|
|
|
|
|
.limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
const lastSimulatedDate = lastSnapshot[0]?.snapshotDate ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
const simulatorInfo = sportsSeason.sport?.simulatorType
|
|
|
|
|
|
? getSimulatorInfo(sportsSeason.sport.simulatorType as SimulatorType)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
2026-03-21 00:12:01 -07:00
|
|
|
|
const lastStandingsSyncedAt =
|
|
|
|
|
|
sportsSeason.sport?.type === "team"
|
|
|
|
|
|
? await getLastSyncedAt(params.id)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
const pendingMappings =
|
|
|
|
|
|
sportsSeason.sport?.type === "team"
|
|
|
|
|
|
? await getPendingStandingsMappings(params.id)
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
2026-03-09 15:34:31 -07:00
|
|
|
|
return {
|
|
|
|
|
|
sportsSeason,
|
|
|
|
|
|
participants,
|
|
|
|
|
|
lastSimulatedDate,
|
|
|
|
|
|
simulatorInfo,
|
2026-03-21 00:12:01 -07:00
|
|
|
|
lastStandingsSyncedAt: lastStandingsSyncedAt?.toISOString() ?? null,
|
|
|
|
|
|
pendingMappings,
|
2025-10-12 21:54:49 -07:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 14:34:09 -07:00
|
|
|
|
export async function action(args: Route.ActionArgs) {
|
|
|
|
|
|
const { request, params } = args;
|
|
|
|
|
|
const { userId } = await getAuth(args);
|
|
|
|
|
|
const isAdmin = userId ? await isUserAdminByClerkId(userId) : false;
|
|
|
|
|
|
if (!isAdmin) {
|
|
|
|
|
|
throw new Response("Forbidden", { status: 403 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
|
const formData = await request.formData();
|
|
|
|
|
|
const intent = formData.get("intent");
|
|
|
|
|
|
|
|
|
|
|
|
if (intent === "delete") {
|
|
|
|
|
|
await deleteSportsSeason(params.id);
|
|
|
|
|
|
return redirect("/admin/sports-seasons");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 14:34:09 -07:00
|
|
|
|
if (intent === "rescore") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const db = database();
|
|
|
|
|
|
const links = await db.query.seasonSports.findMany({
|
|
|
|
|
|
where: eq(seasonSports.sportsSeasonId, params.id),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (links.length === 0) {
|
|
|
|
|
|
return { success: true, intent: "rescore", message: "No linked fantasy seasons found — nothing to rescore." };
|
|
|
|
|
|
}
|
|
|
|
|
|
await Promise.all(links.map((link) => recalculateStandings(link.seasonId, db)));
|
2026-03-18 22:15:28 -07:00
|
|
|
|
await Promise.all(links.map((link) => createDailySnapshot(link.seasonId, db)));
|
2026-03-17 14:34:09 -07:00
|
|
|
|
return { success: true, intent: "rescore", message: `Rescored ${links.length} linked season(s).` };
|
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.error("Error rescoring:", error);
|
2026-03-17 14:34:09 -07:00
|
|
|
|
return { error: "Failed to rescore. Please try again." };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 00:12:01 -07:00
|
|
|
|
if (intent === "sync-standings") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await syncStandings(params.id);
|
|
|
|
|
|
return { success: true, intent: "sync-standings", syncResult: result };
|
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.error("Error syncing standings:", error);
|
2026-03-21 00:12:01 -07:00
|
|
|
|
return {
|
|
|
|
|
|
syncError:
|
|
|
|
|
|
error instanceof Error ? error.message : "Failed to sync standings. Please try again.",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (intent === "resolve-mapping") {
|
|
|
|
|
|
const externalTeamId = formData.get("externalTeamId");
|
|
|
|
|
|
const participantId = formData.get("participantId");
|
|
|
|
|
|
const standingDataRaw = formData.get("standingData");
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
typeof externalTeamId !== "string" ||
|
|
|
|
|
|
!externalTeamId.trim() ||
|
|
|
|
|
|
typeof participantId !== "string" ||
|
|
|
|
|
|
!participantId.trim() ||
|
|
|
|
|
|
typeof standingDataRaw !== "string"
|
|
|
|
|
|
) {
|
|
|
|
|
|
return { error: "Invalid resolve-mapping payload." };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const standingData = JSON.parse(standingDataRaw) as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
|
|
// Write externalId onto the participant for future ID-first matching
|
|
|
|
|
|
await updateParticipant(participantId, { externalId: externalTeamId });
|
|
|
|
|
|
|
|
|
|
|
|
// Upsert the standing record from the stored standingData
|
|
|
|
|
|
await upsertRegularSeasonStandings([
|
|
|
|
|
|
{
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
sportsSeasonId: params.id,
|
|
|
|
|
|
wins: (standingData.wins as number) ?? 0,
|
|
|
|
|
|
losses: (standingData.losses as number) ?? 0,
|
|
|
|
|
|
otLosses: (standingData.otLosses as number | null) ?? null,
|
|
|
|
|
|
ties: (standingData.ties as number | null) ?? null,
|
|
|
|
|
|
winPct: (standingData.winPct as number) ?? 0,
|
|
|
|
|
|
gamesPlayed: (standingData.gamesPlayed as number) ?? 0,
|
|
|
|
|
|
gamesBack: (standingData.gamesBack as number | null) ?? null,
|
|
|
|
|
|
conference: (standingData.conference as string | null) ?? null,
|
|
|
|
|
|
division: (standingData.division as string | null) ?? null,
|
|
|
|
|
|
conferenceRank: (standingData.conferenceRank as number | null) ?? null,
|
|
|
|
|
|
divisionRank: (standingData.divisionRank as number | null) ?? null,
|
|
|
|
|
|
leagueRank: (standingData.leagueRank as number) ?? 0,
|
|
|
|
|
|
streak: (standingData.streak as string | null) ?? null,
|
|
|
|
|
|
lastTen: (standingData.lastTen as string | null) ?? null,
|
|
|
|
|
|
homeRecord: (standingData.homeRecord as string | null) ?? null,
|
|
|
|
|
|
awayRecord: (standingData.awayRecord as string | null) ?? null,
|
|
|
|
|
|
externalTeamId,
|
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from pending queue
|
|
|
|
|
|
await deletePendingStandingsMapping(params.id, externalTeamId);
|
|
|
|
|
|
|
|
|
|
|
|
return { success: true, intent: "resolve-mapping", resolvedTeam: String(standingData.teamName ?? "") };
|
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.error("Error resolving mapping:", error);
|
2026-03-21 00:12:01 -07:00
|
|
|
|
return { error: "Failed to resolve mapping. Please try again." };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
|
if (intent === "finalize-standings") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await processSeasonStandings(params.id);
|
|
|
|
|
|
await updateSportsSeason(params.id, { status: "completed" });
|
2026-03-17 14:34:09 -07:00
|
|
|
|
return { success: true, intent: "finalize-standings", message: "Standings finalized and fantasy placements assigned!" };
|
2026-03-07 21:59:29 -08:00
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.error("Error finalizing standings:", error);
|
2026-03-07 21:59:29 -08:00
|
|
|
|
return { error: "Failed to finalize standings. Please try again." };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
|
// Update
|
|
|
|
|
|
const name = formData.get("name");
|
|
|
|
|
|
const year = formData.get("year");
|
|
|
|
|
|
const startDate = formData.get("startDate");
|
|
|
|
|
|
const endDate = formData.get("endDate");
|
|
|
|
|
|
const status = formData.get("status");
|
|
|
|
|
|
const scoringType = formData.get("scoringType");
|
2025-11-11 10:08:25 -08:00
|
|
|
|
const scoringPattern = formData.get("scoringPattern");
|
|
|
|
|
|
const totalMajors = formData.get("totalMajors");
|
2026-03-18 22:40:19 -07:00
|
|
|
|
const isDraftable = formData.get("isDraftable") === "true";
|
2025-10-12 21:54:49 -07:00
|
|
|
|
|
|
|
|
|
|
// Validation
|
|
|
|
|
|
if (typeof name !== "string" || !name.trim()) {
|
|
|
|
|
|
return { error: "Season name is required" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof year !== "string") {
|
|
|
|
|
|
return { error: "Year is required" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const yearNum = parseInt(year, 10);
|
|
|
|
|
|
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
|
|
|
|
|
|
return { error: "Year must be between 2000 and 2100" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status !== "upcoming" && status !== "active" && status !== "completed") {
|
|
|
|
|
|
return { error: "Invalid status" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (scoringType !== "playoffs" && scoringType !== "regular_season" && scoringType !== "majors") {
|
|
|
|
|
|
return { error: "Invalid scoring type" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
|
const validScoringPatterns = ["playoff_bracket", "season_standings", "qualifying_points"];
|
2025-11-11 10:08:25 -08:00
|
|
|
|
if (scoringPattern && typeof scoringPattern === "string" && !validScoringPatterns.includes(scoringPattern)) {
|
|
|
|
|
|
return { error: "Invalid scoring pattern" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
|
try {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
const updateData: Partial<NewSportsSeason> = {
|
2025-10-12 21:54:49 -07:00
|
|
|
|
name: name.trim(),
|
|
|
|
|
|
year: yearNum,
|
|
|
|
|
|
startDate: typeof startDate === "string" && startDate ? startDate : null,
|
|
|
|
|
|
endDate: typeof endDate === "string" && endDate ? endDate : null,
|
|
|
|
|
|
status,
|
|
|
|
|
|
scoringType,
|
2026-03-18 22:40:19 -07:00
|
|
|
|
isDraftable,
|
2025-11-11 10:08:25 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (scoringPattern && typeof scoringPattern === "string") {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
updateData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points";
|
2025-11-11 10:08:25 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (totalMajors && typeof totalMajors === "string") {
|
|
|
|
|
|
const totalMajorsNum = parseInt(totalMajors, 10);
|
|
|
|
|
|
if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) {
|
|
|
|
|
|
updateData.totalMajors = totalMajorsNum;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await updateSportsSeason(params.id, updateData);
|
2025-10-12 21:54:49 -07:00
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
|
return { success: true, message: "Sports season updated successfully!" };
|
2025-10-12 21:54:49 -07:00
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.error("Error updating sports season:", error);
|
2025-10-12 21:54:49 -07:00
|
|
|
|
return { error: "Failed to update sports season. Please try again." };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function EditSportsSeason({ loaderData, actionData }: Route.ComponentProps) {
|
2026-03-21 00:12:01 -07:00
|
|
|
|
const { sportsSeason, participants, lastSimulatedDate, simulatorInfo, lastStandingsSyncedAt, pendingMappings } = loaderData;
|
2025-10-12 21:54:49 -07:00
|
|
|
|
const navigate = useNavigate();
|
2026-03-21 00:12:01 -07:00
|
|
|
|
const navigation = useNavigation();
|
|
|
|
|
|
const isSyncingStandings =
|
|
|
|
|
|
navigation.state === "submitting" &&
|
|
|
|
|
|
(navigation.formData?.get("intent") as string) === "sync-standings";
|
2025-11-11 10:08:25 -08:00
|
|
|
|
const [scoringPattern, setScoringPattern] = useState<string>(sportsSeason.scoringPattern || "");
|
2025-10-12 21:54:49 -07:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-8">
|
|
|
|
|
|
<div className="max-w-2xl">
|
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<h1 className="text-3xl font-bold">Edit Sports Season</h1>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1">
|
|
|
|
|
|
{sportsSeason.sport.name} - {sportsSeason.name}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Sports Season Details</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Update the information for this sports season
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post" className="space-y-6">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="name">Season Name</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="name"
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
defaultValue={sportsSeason.name}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="year">Year</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="year"
|
|
|
|
|
|
name="year"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="2000"
|
|
|
|
|
|
max="2100"
|
|
|
|
|
|
defaultValue={sportsSeason.year}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="startDate">Start Date (Optional)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="startDate"
|
|
|
|
|
|
name="startDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
defaultValue={sportsSeason.startDate || ""}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="endDate">End Date (Optional)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="endDate"
|
|
|
|
|
|
name="endDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
defaultValue={sportsSeason.endDate || ""}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="status">Status</Label>
|
|
|
|
|
|
<Select name="status" defaultValue={sportsSeason.status} required>
|
|
|
|
|
|
<SelectTrigger id="status">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="upcoming">Upcoming</SelectItem>
|
|
|
|
|
|
<SelectItem value="active">Active</SelectItem>
|
|
|
|
|
|
<SelectItem value="completed">Completed</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="scoringType">Scoring Type</Label>
|
|
|
|
|
|
<Select name="scoringType" defaultValue={sportsSeason.scoringType} required>
|
|
|
|
|
|
<SelectTrigger id="scoringType">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="playoffs">Playoffs</SelectItem>
|
|
|
|
|
|
<SelectItem value="regular_season">Regular Season</SelectItem>
|
|
|
|
|
|
<SelectItem value="majors">Majors</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-11-11 10:08:25 -08:00
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Playoffs: Team sports playoffs. Regular Season: Full season standings. Majors: Individual sport majors.
|
|
|
|
|
|
</p>
|
2025-10-12 21:54:49 -07:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-11 10:08:25 -08:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="scoringPattern">Scoring Pattern (Optional)</Label>
|
|
|
|
|
|
<Select name="scoringPattern" value={scoringPattern} onValueChange={setScoringPattern}>
|
|
|
|
|
|
<SelectTrigger id="scoringPattern">
|
|
|
|
|
|
<SelectValue placeholder="Select scoring pattern (optional)" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2026-03-07 21:59:29 -08:00
|
|
|
|
<SelectItem value="playoff_bracket">Playoff Bracket</SelectItem>
|
2025-11-11 10:08:25 -08:00
|
|
|
|
<SelectItem value="season_standings">Season Standings</SelectItem>
|
|
|
|
|
|
<SelectItem value="qualifying_points">Qualifying Points (Golf/Tennis)</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Qualifying Points: For sports like Golf/Tennis where participants earn points across majors, then top 8 get fantasy points.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{scoringPattern === "qualifying_points" && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="totalMajors">Total Majors</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="totalMajors"
|
|
|
|
|
|
name="totalMajors"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
max="10"
|
|
|
|
|
|
defaultValue={sportsSeason.totalMajors || 4}
|
|
|
|
|
|
placeholder="e.g., 4 (for Golf)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
How many major tournaments will be tracked? (e.g., Golf has 4 majors, Tennis has 4 Grand Slams)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-18 22:40:19 -07:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="isDraftable"
|
|
|
|
|
|
name="isDraftable"
|
|
|
|
|
|
defaultChecked={sportsSeason.isDraftable}
|
|
|
|
|
|
value="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="isDraftable">Available for drafting</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
When unchecked, this season will not appear in league creation or pre-draft league settings.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
|
{actionData?.error && (
|
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
{actionData.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{actionData?.success && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
2026-03-07 21:59:29 -08:00
|
|
|
|
{actionData.message}
|
2025-10-12 21:54:49 -07:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
|
<Button type="submit" className="flex-1">
|
|
|
|
|
|
Save Changes
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="outline" asChild>
|
|
|
|
|
|
<Link to="/admin/sports-seasons">Cancel</Link>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle>Participants</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
{participants.length} {participants.length === 1 ? "participant" : "participants"}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/participants`)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Users className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Manage Participants
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{participants.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
No participants added yet. Add teams or players to this season.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{participants.slice(0, 5).map((participant) => (
|
|
|
|
|
|
<div key={participant.id} className="text-sm">
|
|
|
|
|
|
{participant.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{participants.length > 5 && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
And {participants.length - 5} more...
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2025-11-17 22:19:46 -08:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle>Expected Values</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
2026-03-09 15:34:31 -07:00
|
|
|
|
{simulatorInfo
|
|
|
|
|
|
? simulatorInfo.name
|
|
|
|
|
|
: "Manage probability distributions and projected points"}
|
2025-11-17 22:19:46 -08:00
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
2026-03-09 15:34:31 -07:00
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
{sportsSeason.simulationStatus === "failed" && (
|
|
|
|
|
|
<Badge variant="outline" className="bg-destructive/15 text-destructive border-destructive/30">
|
|
|
|
|
|
<AlertTriangle className="mr-1 h-3 w-3" />
|
|
|
|
|
|
Last run failed
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
2025-11-17 22:19:46 -08:00
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/futures-odds`)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Calculator className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Futures Odds
|
|
|
|
|
|
</Button>
|
2026-03-09 15:34:31 -07:00
|
|
|
|
{simulatorInfo && (
|
|
|
|
|
|
<Form method="post" action={`/admin/sports-seasons/${sportsSeason.id}/simulate`}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
disabled={sportsSeason.simulationStatus === "running"}
|
|
|
|
|
|
>
|
|
|
|
|
|
{sportsSeason.simulationStatus === "running" ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
|
Running...
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Zap className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Run Simulation
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
)}
|
2025-11-17 22:19:46 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-03-09 15:34:31 -07:00
|
|
|
|
{simulatorInfo
|
|
|
|
|
|
? <>
|
|
|
|
|
|
{simulatorInfo.description}.{" "}
|
|
|
|
|
|
{lastSimulatedDate ? `Last simulated: ${lastSimulatedDate}.` : "No simulation has been run yet."}
|
|
|
|
|
|
{" "}Import futures odds first, then run the simulation to update EVs and save a snapshot.
|
|
|
|
|
|
</>
|
|
|
|
|
|
: "Import futures odds to set probability distributions."}
|
2025-11-17 22:19:46 -08:00
|
|
|
|
</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle>Scoring Events</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
2026-03-07 21:59:29 -08:00
|
|
|
|
Manage games, tournaments, and schedule entries
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/events`)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trophy className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Manage Events
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-03-07 21:59:29 -08:00
|
|
|
|
Create playoff brackets, major tournaments, or import a race/game schedule.
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-03-21 00:12:01 -07:00
|
|
|
|
{sportsSeason.sport?.type === "team" && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle>Regular Season Standings</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Sync current W/L standings from the official API, or edit manually.
|
|
|
|
|
|
{lastStandingsSyncedAt && (
|
|
|
|
|
|
<span className="ml-1 text-muted-foreground">
|
|
|
|
|
|
Last synced: {new Date(lastStandingsSyncedAt).toLocaleString()}.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/regular-standings`)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Edit Manually
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="sync-standings" />
|
|
|
|
|
|
<Button type="submit" size="sm" disabled={isSyncingStandings}>
|
|
|
|
|
|
{isSyncingStandings ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
|
Syncing...
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Sync Standings
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
{(actionData?.success && actionData.intent === "sync-standings") || actionData?.syncError ? (
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{actionData?.success && actionData.intent === "sync-standings" && actionData.syncResult && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
Synced {actionData.syncResult.synced} team{actionData.syncResult.synced !== 1 ? "s" : ""} successfully.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{actionData.syncResult.unmatched.length > 0 && (
|
|
|
|
|
|
<div className="bg-amber-500/15 text-amber-600 dark:text-amber-400 px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
<p className="font-medium mb-1">
|
|
|
|
|
|
{actionData.syncResult.unmatched.length} team{actionData.syncResult.unmatched.length !== 1 ? "s" : ""} could not be matched to participants:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="list-disc list-inside space-y-0.5">
|
|
|
|
|
|
{actionData.syncResult.unmatched.map((u: { teamName: string; externalTeamId: string }) => (
|
|
|
|
|
|
<li key={u.externalTeamId}>{u.teamName}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p className="mt-2 text-xs">
|
|
|
|
|
|
Use the "Unmatched Teams" card below to assign these to participants. Future syncs will use the saved ID.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{actionData?.syncError && (
|
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
{actionData.syncError}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{sportsSeason.sport?.type === "team" && pendingMappings.length > 0 && (
|
|
|
|
|
|
<Card className="border-amber-500/30">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
|
|
|
|
Unmatched Teams ({pendingMappings.length})
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
These teams from the last sync could not be automatically matched to a participant.
|
|
|
|
|
|
Assign each one to the correct participant to resolve.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
|
{actionData?.success && actionData.intent === "resolve-mapping" && (
|
|
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
Resolved: {actionData.resolvedTeam}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{pendingMappings.map((mapping) => (
|
|
|
|
|
|
<Form key={mapping.externalTeamId} method="post" className="flex items-center gap-3">
|
|
|
|
|
|
<input type="hidden" name="intent" value="resolve-mapping" />
|
|
|
|
|
|
<input type="hidden" name="externalTeamId" value={mapping.externalTeamId} />
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name="standingData"
|
|
|
|
|
|
value={JSON.stringify(mapping.standingData)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<p className="text-sm font-medium truncate">{mapping.teamName}</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">ID: {mapping.externalTeamId}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Select name="participantId" required>
|
|
|
|
|
|
<SelectTrigger className="w-56">
|
|
|
|
|
|
<SelectValue placeholder="Select participant…" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{participants.map((p) => (
|
|
|
|
|
|
<SelectItem key={p.id} value={p.id}>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Button type="submit" size="sm">
|
|
|
|
|
|
Resolve
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
|
{sportsSeason.scoringPattern === "season_standings" && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle>Championship Standings</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Update participant positions and points as the season progresses
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/standings`)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Update Standings
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{sportsSeason.scoringPattern === "season_standings" && (
|
|
|
|
|
|
<Card className={sportsSeason.status === "completed" ? "border-emerald-500/30" : "border-amber-500/30"}>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
|
Finalize Standings
|
|
|
|
|
|
{sportsSeason.status === "completed" && (
|
|
|
|
|
|
<Badge variant="outline" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">
|
|
|
|
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
|
|
|
|
Finalized
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
{sportsSeason.status === "completed"
|
|
|
|
|
|
? "Season standings have been finalized and fantasy placements assigned."
|
|
|
|
|
|
: "When the championship is over, finalize standings to assign fantasy placements (1st–8th) to participants based on their final positions."}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
{sportsSeason.status !== "completed" && (
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{actionData?.error && (
|
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm mb-4">
|
|
|
|
|
|
{actionData.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-17 14:34:09 -07:00
|
|
|
|
{actionData?.success && actionData.intent === "finalize-standings" && (
|
2026-03-07 21:59:29 -08:00
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
|
|
|
|
|
|
{actionData.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
|
<Button variant="outline" className="border-amber-500/50 text-amber-600 hover:bg-amber-500/10 dark:text-amber-400">
|
|
|
|
|
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Finalize Standings
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>Finalize championship standings?</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
This will read the current participant standings and assign fantasy placements (1st through 8th place). The season will be marked as completed. This action can be re-run to correct results if needed.
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="finalize-standings" />
|
|
|
|
|
|
<AlertDialogAction type="submit">
|
|
|
|
|
|
Finalize Standings
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-17 14:34:09 -07:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Fantasy Standings</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Force a recalculation of all fantasy standings linked to this sports season. Use this after fixing scoring bugs or data corrections.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{actionData?.success && actionData.intent === "rescore" && (
|
|
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
|
|
|
|
|
|
{actionData.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="rescore" />
|
|
|
|
|
|
<Button type="submit" variant="outline" className="border-blue-500/50 text-blue-600 hover:bg-blue-500/10 dark:text-blue-400">
|
|
|
|
|
|
Force Re-score
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
|
<Card className="border-destructive">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-destructive">Danger Zone</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Permanently delete this sports season
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
|
<Button variant="destructive">
|
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Delete Sports Season
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
This will permanently delete the sports season "{sportsSeason.name}" and all
|
|
|
|
|
|
associated participants and results. This action cannot be undone.
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="delete" />
|
|
|
|
|
|
<AlertDialogAction type="submit" className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
|
|
|
|
Delete
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|