* fix: remove probability sum validation and add batch save with total EV - Remove the requirement that manual probabilities sum to 1.0, allowing partial distributions (e.g. when a participant has <100% chance of top 8) - Replace per-row save buttons with a single "Save All" button that submits all participants at once - Add a Total EV row at the bottom of the table summing all participant EVs - Update action to batch process all participants from a single form submission https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy * fix: validate participantIds server-side instead of trusting form input Fetch participants from the DB in the action using the season ID from the URL params, eliminating the untrusted participantIds hidden field. Remove the now-unused hidden input from the frontend form. https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy --------- Co-authored-by: Claude <noreply@anthropic.com>
86 lines
3 KiB
TypeScript
86 lines
3 KiB
TypeScript
import type { Route } from "./+types/admin.sports-seasons.$id.expected-values";
|
|
import { findSportsSeasonById } from "~/models/sports-season";
|
|
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
|
import {
|
|
upsertParticipantEV,
|
|
getAllParticipantEVsForSeason
|
|
} from "~/models/participant-expected-value";
|
|
|
|
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);
|
|
const existingEVs = await getAllParticipantEVsForSeason(params.id);
|
|
|
|
// Create a map of participant ID to EV data
|
|
const evMap = new Map(existingEVs.map(ev => [ev.participantId, ev]));
|
|
|
|
return {
|
|
sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string } },
|
|
participants,
|
|
existingEVs: evMap,
|
|
};
|
|
}
|
|
|
|
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 participants = await findParticipantsBySportsSeasonId(params.id);
|
|
const participantIds = participants.map((p: { id: string }) => p.id);
|
|
|
|
try {
|
|
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 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 {
|
|
error: error instanceof Error ? error.message : "Failed to save probabilities"
|
|
};
|
|
}
|
|
}
|