Fix qualifying points league display.

This commit is contained in:
Chris Parsons 2026-04-12 17:06:20 -07:00
parent 1798020c74
commit cf91de941b
2 changed files with 71 additions and 36 deletions

View file

@ -1,5 +1,6 @@
import { Form } from "react-router"; import { Form } from "react-router";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { TeamOwnerBadge } from "~/components/ui/team-owner-badge";
import { import {
Card, Card,
CardContent, CardContent,
@ -17,6 +18,13 @@ import {
} from "~/components/ui/table"; } from "~/components/ui/table";
import { Trophy, CheckCircle2 } from "lucide-react"; import { Trophy, CheckCircle2 } from "lucide-react";
interface TeamOwnership {
participantId: string;
teamName: string;
teamId: string;
ownerName?: string;
}
interface QPStanding { interface QPStanding {
id: string; id: string;
totalQualifyingPoints: string; totalQualifyingPoints: string;
@ -46,6 +54,7 @@ interface QualifyingPointsStandingsProps {
totalMajors?: number | null; totalMajors?: number | null;
majorsCompleted?: number; majorsCompleted?: number;
canFinalize: boolean; // Whether all majors are complete canFinalize: boolean; // Whether all majors are complete
teamOwnerships?: TeamOwnership[];
} }
export function QualifyingPointsStandings({ export function QualifyingPointsStandings({
@ -55,53 +64,61 @@ export function QualifyingPointsStandings({
totalMajors, totalMajors,
majorsCompleted = 0, majorsCompleted = 0,
canFinalize, canFinalize,
teamOwnerships = [],
}: QualifyingPointsStandingsProps) { }: QualifyingPointsStandingsProps) {
// Calculate projected fantasy points for each standing (Q6) const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
const calculateProjectedPoints = (currentRank: number): number => { const pointsMap: Record<number, number> = scoringRules
if (!scoringRules || currentRank < 1 || currentRank > 8) return 0; ? {
1: scoringRules.pointsFor1st,
const pointsMap: Record<number, number> = { 2: scoringRules.pointsFor2nd,
1: scoringRules.pointsFor1st, 3: scoringRules.pointsFor3rd,
2: scoringRules.pointsFor2nd, 4: scoringRules.pointsFor4th,
3: scoringRules.pointsFor3rd, 5: scoringRules.pointsFor5th,
4: scoringRules.pointsFor4th, 6: scoringRules.pointsFor6th,
5: scoringRules.pointsFor5th, 7: scoringRules.pointsFor7th,
6: scoringRules.pointsFor6th, 8: scoringRules.pointsFor8th,
7: scoringRules.pointsFor7th, }
8: scoringRules.pointsFor8th, : {};
};
return pointsMap[currentRank] || 0;
};
// Assign current ranks with tie handling // Assign current ranks with tie handling
const rankedStandings: Array<typeof standings[0] & { currentRank: number }> = []; const rankedStandings: Array<typeof standings[0] & { currentRank: number }> = [];
let previousRank = 0;
let previousQP = -1; let previousQP = -1;
let previousRankStart = -1;
for (let index = 0; index < standings.length; index++) { for (let index = 0; index < standings.length; index++) {
const standing = standings[index]; const standing = standings[index];
const currentQP = parseFloat(standing.totalQualifyingPoints); const currentQP = parseFloat(standing.totalQualifyingPoints);
// Check if this is a tie with the previous participant
let currentRank: number; let currentRank: number;
if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) { if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) {
// Same QP as previous = same rank currentRank = previousRankStart;
currentRank = previousRank;
} else { } else {
// Different QP = new rank (1-based position)
currentRank = index + 1; currentRank = index + 1;
} }
rankedStandings.push({ rankedStandings.push({ ...standing, currentRank });
...standing,
currentRank,
});
previousRank = currentRank; previousRankStart = currentRank;
previousQP = currentQP; previousQP = currentQP;
} }
// Pre-compute tied counts per rank to avoid O(N²) scanning per row
const tiedCountByRank = new Map<number, number>();
for (const s of rankedStandings) {
tiedCountByRank.set(s.currentRank, (tiedCountByRank.get(s.currentRank) ?? 0) + 1);
}
// Calculate EV for a rank by averaging points across all tied positions
const calculateEV = (currentRank: number): number => {
if (!scoringRules || currentRank > 8) return 0;
const tiedCount = tiedCountByRank.get(currentRank) ?? 1;
let total = 0;
for (let i = 0; i < tiedCount; i++) {
total += pointsMap[currentRank + i] ?? 0;
}
return total / tiedCount;
};
return ( return (
<Card className={isFinalized ? "border-emerald-500/30" : "border-amber-accent/30"}> <Card className={isFinalized ? "border-emerald-500/30" : "border-amber-accent/30"}>
<CardHeader> <CardHeader>
@ -120,7 +137,7 @@ export function QualifyingPointsStandings({
Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed. Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed.
{scoringRules ? ( {scoringRules ? (
<span className="block mt-1"> <span className="block mt-1">
Projected fantasy points show what participants would earn if finalized now. Projected points account for ties tied participants share the available points equally.
</span> </span>
) : ( ) : (
<span className="block mt-1 text-muted-foreground"> <span className="block mt-1 text-muted-foreground">
@ -143,6 +160,9 @@ export function QualifyingPointsStandings({
<TableRow> <TableRow>
<TableHead className="w-20">Rank</TableHead> <TableHead className="w-20">Rank</TableHead>
<TableHead>Participant</TableHead> <TableHead>Participant</TableHead>
{teamOwnerships.length > 0 && (
<TableHead className="w-40">Owner</TableHead>
)}
<TableHead className="w-28 text-right">Total QP</TableHead> <TableHead className="w-28 text-right">Total QP</TableHead>
<TableHead className="w-24 text-right">Events</TableHead> <TableHead className="w-24 text-right">Events</TableHead>
{scoringRules && !isFinalized && ( {scoringRules && !isFinalized && (
@ -151,15 +171,11 @@ export function QualifyingPointsStandings({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rankedStandings.map((standing, idx) => { {rankedStandings.map((standing) => {
const projectedPoints = calculateProjectedPoints(standing.currentRank); const projectedPoints = calculateEV(standing.currentRank);
const isTop8 = standing.currentRank <= 8; const isTop8 = standing.currentRank <= 8;
// Check if this is a tie (same rank as another participant) const isTied = (tiedCountByRank.get(standing.currentRank) ?? 1) > 1;
const isTied = rankedStandings.some((other, otherIdx) =>
otherIdx !== idx &&
other.currentRank === standing.currentRank
);
return ( return (
<TableRow key={standing.id} className={!isTop8 ? "opacity-60" : ""}> <TableRow key={standing.id} className={!isTop8 ? "opacity-60" : ""}>
@ -184,6 +200,21 @@ export function QualifyingPointsStandings({
<TableCell className="font-medium"> <TableCell className="font-medium">
{standing.participant.name} {standing.participant.name}
</TableCell> </TableCell>
{teamOwnerships.length > 0 && (
<TableCell>
{(() => {
const ownership = ownershipMap.get(standing.participant.id);
return ownership ? (
<TeamOwnerBadge
teamName={ownership.teamName}
ownerName={ownership.ownerName}
/>
) : (
<span className="text-xs text-muted-foreground">Undrafted</span>
);
})()}
</TableCell>
)}
<TableCell className="text-right"> <TableCell className="text-right">
<span className="font-semibold text-amber-accent"> <span className="font-semibold text-amber-accent">
{parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP {parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP
@ -196,7 +227,10 @@ export function QualifyingPointsStandings({
<TableCell className="text-right"> <TableCell className="text-right">
{isTop8 ? ( {isTop8 ? (
<span className="font-semibold text-emerald-400"> <span className="font-semibold text-emerald-400">
{projectedPoints} pts {projectedPoints % 1 === 0
? projectedPoints
: projectedPoints.toFixed(1)}{" "}
pts
</span> </span>
) : ( ) : (
<span className="text-muted-foreground">0 pts</span> <span className="text-muted-foreground">0 pts</span>

View file

@ -181,6 +181,7 @@ export function SportSeasonDisplay({
totalMajors={totalMajors} totalMajors={totalMajors}
majorsCompleted={majorsCompleted} majorsCompleted={majorsCompleted}
canFinalize={canFinalize} canFinalize={canFinalize}
teamOwnerships={showOwnership ? teamOwnerships : []}
/> />
); );