diff --git a/app/models/participant-expected-value.ts b/app/models/participant-expected-value.ts index a4f3296..e3f4ada 100644 --- a/app/models/participant-expected-value.ts +++ b/app/models/participant-expected-value.ts @@ -9,7 +9,7 @@ import { database } from "~/database/context"; import { participantExpectedValues, participants } from "~/database/schema"; import { eq, and } from "drizzle-orm"; import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator"; -import { calculateEV, validateProbabilities, normalizeProbabilities } from "~/services/ev-calculator"; +import { calculateEV, normalizeProbabilities } from "~/services/ev-calculator"; export type ProbabilitySource = "manual" | "futures_odds" | "elo_simulation" | "performance_model"; @@ -59,15 +59,6 @@ export async function upsertParticipantEV( ): Promise { const { participantId, sportsSeasonId, probabilities, scoringRules, source = "manual", sourceOdds } = input; - // Validate probabilities (only for manual entry) - // For futures_odds/ICM, probabilities may sum to <1.0 for teams unlikely to finish top 8 - if (source === "manual" && !validateProbabilities(probabilities)) { - const sum = Object.values(probabilities).reduce((a, b) => a + b, 0); - throw new Error( - `Probabilities must sum to 1.0 (±0.01). Current sum: ${sum.toFixed(4)} (${(sum * 100).toFixed(1)}%)` - ); - } - // Always validate probabilities don't exceed 1.0 const sum = Object.values(probabilities).reduce((a, b) => a + b, 0); if (sum > 1.01) { diff --git a/app/routes/admin.sports-seasons.$id.expected-values.server.ts b/app/routes/admin.sports-seasons.$id.expected-values.server.ts index bafd46a..7435dcc 100644 --- a/app/routes/admin.sports-seasons.$id.expected-values.server.ts +++ b/app/routes/admin.sports-seasons.$id.expected-values.server.ts @@ -5,7 +5,6 @@ import { upsertParticipantEV, getAllParticipantEVsForSeason } from "~/models/participant-expected-value"; -import type { ProbabilitySource } from "~/models/participant-expected-value"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -27,62 +26,57 @@ export async function loader({ params }: Route.LoaderArgs) { }; } +const scoringRules = { + pointsFor1st: 100, + pointsFor2nd: 70, + pointsFor3rd: 50, + pointsFor4th: 40, + pointsFor5th: 25, + pointsFor6th: 25, + pointsFor7th: 15, + pointsFor8th: 15, +}; + export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); - const participantId = formData.get("participantId"); - const source = (formData.get("source") || "manual") as ProbabilitySource; - if (typeof participantId !== "string") { - return { error: "Participant ID is required" }; - } - - // Get probability values (entered as percentages, convert to decimals) - const probFirst = parseFloat(formData.get("probFirst") as string || "0") / 100; - const probSecond = parseFloat(formData.get("probSecond") as string || "0") / 100; - const probThird = parseFloat(formData.get("probThird") as string || "0") / 100; - const probFourth = parseFloat(formData.get("probFourth") as string || "0") / 100; - const probFifth = parseFloat(formData.get("probFifth") as string || "0") / 100; - const probSixth = parseFloat(formData.get("probSixth") as string || "0") / 100; - const probSeventh = parseFloat(formData.get("probSeventh") as string || "0") / 100; - const probEighth = parseFloat(formData.get("probEighth") as string || "0") / 100; - - // Use default scoring rules - // Note: Actual league seasons may have different scoring rules - // EVs will be recalculated when used in a specific league - const scoringRules = { - pointsFor1st: 100, - pointsFor2nd: 70, - pointsFor3rd: 50, - pointsFor4th: 40, - pointsFor5th: 25, - pointsFor6th: 25, - pointsFor7th: 15, - pointsFor8th: 15, - }; + const participants = await findParticipantsBySportsSeasonId(params.id); + const participantIds = participants.map((p: { id: string }) => p.id); try { - const result = await upsertParticipantEV({ - participantId, - sportsSeasonId: params.id, - probabilities: { - probFirst, - probSecond, - probThird, - probFourth, - probFifth, - probSixth, - probSeventh, - probEighth, - }, - scoringRules, - source, - }); + const results = await Promise.all( + participantIds.map(async (participantId) => { + const probFirst = parseFloat(formData.get(`probFirst_${participantId}`) as string || "0") / 100; + const probSecond = parseFloat(formData.get(`probSecond_${participantId}`) as string || "0") / 100; + const probThird = parseFloat(formData.get(`probThird_${participantId}`) as string || "0") / 100; + const probFourth = parseFloat(formData.get(`probFourth_${participantId}`) as string || "0") / 100; + const probFifth = parseFloat(formData.get(`probFifth_${participantId}`) as string || "0") / 100; + const probSixth = parseFloat(formData.get(`probSixth_${participantId}`) as string || "0") / 100; + const probSeventh = parseFloat(formData.get(`probSeventh_${participantId}`) as string || "0") / 100; + const probEighth = parseFloat(formData.get(`probEighth_${participantId}`) as string || "0") / 100; - return { - success: true, - participantId, - expectedValue: parseFloat(result.expectedValue), - }; + return upsertParticipantEV({ + participantId, + sportsSeasonId: params.id, + probabilities: { + probFirst, + probSecond, + probThird, + probFourth, + probFifth, + probSixth, + probSeventh, + probEighth, + }, + scoringRules, + source: "manual", + }); + }) + ); + + const totalEV = results.reduce((sum, r) => sum + parseFloat(r.expectedValue), 0); + + return { success: true, totalEV }; } catch (error) { console.error("Error saving probabilities:", error); return { diff --git a/app/routes/admin.sports-seasons.$id.expected-values.tsx b/app/routes/admin.sports-seasons.$id.expected-values.tsx index b95833f..5358883 100644 --- a/app/routes/admin.sports-seasons.$id.expected-values.tsx +++ b/app/routes/admin.sports-seasons.$id.expected-values.tsx @@ -25,6 +25,11 @@ export { loader, action }; export default function ExpectedValuesPage({ loaderData, actionData }: Route.ComponentProps) { const { sportsSeason, participants, existingEVs } = loaderData; + const totalEV = Array.from(existingEVs.values()).reduce( + (sum, ev) => sum + parseFloat(ev.expectedValue), + 0 + ); + return (
@@ -54,7 +59,7 @@ export default function ExpectedValuesPage({ loaderData, actionData }: Route.Com )} {actionData?.success && (
- Successfully saved! Expected Value: {actionData.expectedValue?.toFixed(2)} points + All participants saved successfully!
)} @@ -75,150 +80,147 @@ export default function ExpectedValuesPage({ loaderData, actionData }: Route.Com

- - - - Participant - 1st % - 2nd % - 3rd % - 4th % - 5th % - 6th % - 7th % - 8th % - EV - - - - - {participants.map((participant: { id: string; name: string }) => { - const existingEV = existingEVs.get(participant.id); - const formId = `form-${participant.id}`; +
- return ( - - {participant.name} - - - - - - - - - - - - - - - - - - - - - - - - - - {existingEV ? parseFloat(existingEV.expectedValue).toFixed(2) : "-"} - - - - - - - - +
+ + + Participant + 1st % + 2nd % + 3rd % + 4th % + 5th % + 6th % + 7th % + 8th % + EV + + + + {participants.map((participant: { id: string; name: string }) => { + const existingEV = existingEVs.get(participant.id); + const id = participant.id; + + return ( + + {participant.name} + + + + + + + + + + + + + + + + + + + + + + + + + + {existingEV ? parseFloat(existingEV.expectedValue).toFixed(2) : "-"} + + + ); + })} + {existingEVs.size > 0 && ( + + Total EV + {totalEV.toFixed(2)} - ); - })} - -
+ )} + + - {participants.length === 0 && ( -

- No participants found. Please add participants to this sports season first. -

- )} + {participants.length === 0 ? ( +

+ No participants found. Please add participants to this sports season first. +

+ ) : ( +
+ +
+ )} +