Merge pull request 'Fix QP standings: filter to drafted/points-earning participants with correct global ranks' (#76) from fix/qp-standings-filter-drafted-and-points into main
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 2m33s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m19s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped

Reviewed-on: #76
This commit is contained in:
chrisp 2026-06-06 16:51:47 +00:00
commit 350620ca67
7 changed files with 55 additions and 32 deletions

View file

@ -23,6 +23,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "14.67", totalQualifyingPoints: "14.67",
eventsScored: 1, eventsScored: 1,
finalRanking: null, finalRanking: null,
globalRank: 1,
participant: { id: "participant-1", name: "Player One" }, participant: { id: "participant-1", name: "Player One" },
}, },
{ {
@ -30,6 +31,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "0.50", totalQualifyingPoints: "0.50",
eventsScored: 1, eventsScored: 1,
finalRanking: null, finalRanking: null,
globalRank: 2,
participant: { id: "participant-2", name: "Player Two" }, participant: { id: "participant-2", name: "Player Two" },
}, },
{ {
@ -37,6 +39,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "0.43", totalQualifyingPoints: "0.43",
eventsScored: 1, eventsScored: 1,
finalRanking: null, finalRanking: null,
globalRank: 3,
participant: { id: "participant-3", name: "Player Three" }, participant: { id: "participant-3", name: "Player Three" },
}, },
]} ]}

View file

@ -82,7 +82,7 @@ describe("SportsSection", () => {
await user.click(screen.getByText("2025 MLB Season")); await user.click(screen.getByText("2025 MLB Season"));
expect(mlbCheckbox).toBeChecked(); expect(mlbCheckbox).toBeChecked();
}); }, 15000);
it("submits the currently selected sports seasons after toggling", async () => { it("submits the currently selected sports seasons after toggling", async () => {
const user = userEvent.setup(); const user = userEvent.setup();

View file

@ -23,6 +23,7 @@ interface QPStanding {
totalQualifyingPoints: string; totalQualifyingPoints: string;
eventsScored: number; eventsScored: number;
finalRanking: number | null; finalRanking: number | null;
globalRank: number;
participant: { participant: {
id: string; id: string;
name: string; name: string;
@ -70,26 +71,11 @@ export function QualifyingPointsStandings({
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
const userParticipantSet = new Set(userParticipantIds); const userParticipantSet = new Set(userParticipantIds);
// Assign current ranks with tie handling // Use the pre-computed global rank from the loader so displayed ranks reflect the full
const rankedStandings: Array<QPStanding & { currentRank: number }> = []; // participant field even when the array has been filtered down to drafted/points-earning rows.
let previousQP = -1; const rankedStandings: Array<QPStanding & { currentRank: number }> = standings.map(
let previousRankStart = -1; (standing) => ({ ...standing, currentRank: standing.globalRank })
);
for (let index = 0; index < standings.length; index++) {
const standing = standings[index];
const currentQP = parseFloat(standing.totalQualifyingPoints);
let currentRank: number;
if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) {
currentRank = previousRankStart;
} else {
currentRank = index + 1;
}
rankedStandings.push({ ...standing, currentRank });
previousRankStart = currentRank;
previousQP = currentQP;
}
const tiedCountByRank = new Map<number, number>(); const tiedCountByRank = new Map<number, number>();
for (const s of rankedStandings) { for (const s of rankedStandings) {

View file

@ -64,6 +64,7 @@ interface QPStanding {
totalQualifyingPoints: string; totalQualifyingPoints: string;
eventsScored: number; eventsScored: number;
finalRanking: number | null; finalRanking: number | null;
globalRank: number;
participant: { participant: {
id: string; id: string;
name: string; name: string;

View file

@ -25,11 +25,25 @@ export async function loader({ params }: Route.LoaderArgs) {
const events = await getScoringEventsForSportsSeason(params.id); const events = await getScoringEventsForSportsSeason(params.id);
// For qualifying sports seasons, get QP standings // For qualifying sports seasons, get QP standings with global ranks attached
let qpStandings = null; let qpStandings = null;
const scoringRules = null; const scoringRules = null;
if (sportsSeason.scoringPattern === "qualifying_points") { if (sportsSeason.scoringPattern === "qualifying_points") {
qpStandings = await getQPStandings(params.id); const standings = await getQPStandings(params.id);
let prevQP = -1;
let prevRankStart = 1;
qpStandings = standings.map((s, index) => {
const qp = parseFloat(s.totalQualifyingPoints);
let rank: number;
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
rank = prevRankStart;
} else {
rank = index + 1;
prevRankStart = rank;
}
prevQP = qp;
return { ...s, globalRank: rank };
});
} }
// Tournaments for this sport that don't already have a scoring event in this season. // Tournaments for this sport that don't already have a scoring event in this season.

View file

@ -155,7 +155,7 @@ export async function loader(args: Route.LoaderArgs) {
let participantPoints: { participantId: string; points: number }[] = []; let participantPoints: { participantId: string; points: number }[] = [];
let partialScoreParticipantIds: string[] = []; let partialScoreParticipantIds: string[] = [];
let seasonStandings: SeasonStanding[] = []; let seasonStandings: SeasonStanding[] = [];
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number]; type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number] & { globalRank: number };
let qpStandings: QPStanding[] = []; let qpStandings: QPStanding[] = [];
// Group standings for group-stage events (e.g. FIFA World Cup) // Group standings for group-stage events (e.g. FIFA World Cup)
@ -293,9 +293,28 @@ export async function loader(args: Route.LoaderArgs) {
}, },
})); }));
} else if (scoringPattern === "qualifying_points") { } else if (scoringPattern === "qualifying_points") {
// Fetch qualifying points standings
const standings = await getQPStandings(sportsSeasonId); const standings = await getQPStandings(sportsSeasonId);
qpStandings = standings; // Compute global ranks with tie handling across the full field before filtering,
// so the displayed rank numbers remain correct after undrafted/zero-QP rows are removed.
let prevQP = -1;
let prevRankStart = 1;
const withGlobalRank = standings.map((s, index) => {
const qp = parseFloat(s.totalQualifyingPoints);
let rank: number;
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
rank = prevRankStart;
} else {
rank = index + 1;
prevRankStart = rank;
}
prevQP = qp;
return { ...s, globalRank: rank };
});
qpStandings = withGlobalRank.filter(
(s) =>
parseFloat(s.totalQualifyingPoints) > 0 ||
ownershipMap.has(s.participant.id)
);
} }
// Fetch event schedule for all patterns (upcoming + recent) // Fetch event schedule for all patterns (upcoming + recent)

View file

@ -114,7 +114,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(20);
await expect(sim.simulate("season-1")).rejects.toThrow("No participants found"); await expect(sim.simulate("season-1")).rejects.toThrow("No participants found");
}); });
@ -125,7 +125,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(20);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
expect(results).toHaveLength(48); expect(results).toHaveLength(48);
const ids = new Set(results.map((r) => r.participantId)); const ids = new Set(results.map((r) => r.participantId));
@ -141,7 +141,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(20);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0); const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
@ -162,7 +162,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(50);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
// probFirst + probSecond + probThird + probFourth should cover all probability mass // probFirst + probSecond + probThird + probFourth should cover all probability mass
@ -218,7 +218,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(20);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
const p3Result = results.find((r) => r.participantId === "p3"); const p3Result = results.find((r) => r.participantId === "p3");
@ -238,7 +238,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100); const sim = new WorldCupSimulator(20);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
for (const r of results) { for (const r of results) {