Fix qualifying points league display.
This commit is contained in:
parent
1798020c74
commit
cf91de941b
2 changed files with 71 additions and 36 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ export function SportSeasonDisplay({
|
||||||
totalMajors={totalMajors}
|
totalMajors={totalMajors}
|
||||||
majorsCompleted={majorsCompleted}
|
majorsCompleted={majorsCompleted}
|
||||||
canFinalize={canFinalize}
|
canFinalize={canFinalize}
|
||||||
|
teamOwnerships={showOwnership ? teamOwnerships : []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue