import { Form, Link, useLoaderData, useActionData } from "react-router"; import type { LoaderFunctionArgs, ActionFunctionArgs } from "react-router"; import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantResultsBySportsSeasonId } from "~/models/participant-result"; import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value"; import { updateProbabilitiesAfterResult, previewProbabilityUpdate, type ProbabilityComparison, } from "~/services/probability-updater"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; import { ArrowLeft, RefreshCw, CheckCircle, AlertTriangle } from "lucide-react"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq } from "drizzle-orm"; export async function loader({ params }: LoaderFunctionArgs) { if (!params.id) { throw new Response("Missing season ID", { status: 400 }); } const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) { throw new Response("Sports season not found", { status: 404 }); } const results = await findParticipantResultsBySportsSeasonId(params.id); const existingEVs = await getAllParticipantEVsForSeason(params.id); // Get preview of what would change const preview = await previewProbabilityUpdate(params.id); // Get participant names for display const db = database(); const participants = await db.query.participants.findMany({ where: (participants, { inArray }) => inArray( participants.id, preview.map((p: ProbabilityComparison) => p.participantId) ), }); const participantMap = new Map(participants.map((p: typeof participants[0]) => [p.id, p])); return { sportsSeason, results, existingEVs, preview: preview.map((p: ProbabilityComparison) => ({ ...p, participantName: participantMap.get(p.participantId)?.name || "Unknown", })), }; } export async function action({ params, request }: ActionFunctionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "recalculate") { const recalculateUnfinished = formData.get("recalculateUnfinished") === "true"; if (!params.id) { return { success: false, error: "Missing season ID" }; } const result = await updateProbabilitiesAfterResult( params.id, recalculateUnfinished ); if (result.errors.length > 0) { return { success: false, error: result.errors.join("; "), result, }; } return { success: true, result, }; } return { success: false, error: "Invalid intent" }; } export default function RecalculateProbabilities() { const { sportsSeason, results, existingEVs, preview } = useLoaderData(); const actionData = useActionData(); const finishedCount = results.filter((r) => r.finalPosition !== null).length; const totalCount = existingEVs.length; const unfinishedCount = totalCount - finishedCount; const changesCount = preview.filter((p) => p.status === "finished").length; // Check for invalid probabilities (> 1.0) const invalidProbabilities = preview.filter((p) => p.before.some((prob) => prob > 1.0) ); function formatProbability(prob: number): string { return (prob * 100).toFixed(1) + "%"; } function getProbabilityChange(before: number, after: number): string { const diff = (after - before) * 100; if (Math.abs(diff) < 0.1) return "—"; const sign = diff > 0 ? "+" : ""; return sign + diff.toFixed(1) + "%"; } return (

Recalculate Probabilities

{sportsSeason.name} — {sportsSeason.year}

{invalidProbabilities.length > 0 && (
Invalid Probability Data Detected
{invalidProbabilities.length} participant(s) have probabilities greater than 100%, which indicates corrupted data

The following participants have invalid probabilities (marked with ⚠️ in the table below):

    {invalidProbabilities.slice(0, 5).map((p) => (
  • {p.participantName}: P(1st) = {formatProbability(p.before[0])}
  • ))} {invalidProbabilities.length > 5 && (
  • ... and {invalidProbabilities.length - 5} more
  • )}

Solution: Go to the{" "} Futures Odds page {" "} and regenerate probabilities from current betting odds to fix this data.

)} {actionData?.success ? (
Probabilities Updated Successfully
{actionData.result?.updated} participant {actionData.result?.updated !== 1 ? "s" : ""} updated
Finished participants: {actionData.result?.finishedParticipants}
Recalculated participants: {actionData.result?.unfishedParticipants}
) : actionData?.error ? (
Error
{actionData.error}
) : null} Summary Current state of participant results and probabilities
{finishedCount}
Finished Participants
{unfinishedCount}
Unfinished Participants
{changesCount}
Changes to Apply
{changesCount === 0 ? ( No Changes Needed All finished participants already have their probabilities set correctly.

Either no participants have finished yet, or their probabilities are already up to date.

) : ( <> Probability Changes Preview of changes that will be applied Participant Status P(1st) Before P(1st) After P(2nd) After P(3rd) After {preview .filter((p: ProbabilityComparison) => p.status === "finished") .map((p: ProbabilityComparison) => { const result = results.find( (r: typeof results[0]) => r.participantId === p.participantId ); const finalPosition = result?.finalPosition; return ( {p.participantName} Finished {finalPosition ? `${finalPosition}${ finalPosition === 1 ? "st" : finalPosition === 2 ? "nd" : finalPosition === 3 ? "rd" : "th" }` : ""}
1 ? "text-destructive" : ""}> {formatProbability(p.before[0])} {p.before[0] > 1 && " ⚠️"}
{formatProbability(p.after[0])}
{getProbabilityChange(p.before[0], p.after[0])}
{formatProbability(p.after[1])}
{formatProbability(p.after[2])}
); })}
Apply Changes Update probabilities based on real results

What happens:

  • • Finished participants get 100% probability at their final position
  • • Unfinished participants get recalculated probabilities based on remaining competition
  • • Expected values will be automatically updated for all affected leagues
)}
); }