diff --git a/app/components/scoring/QualifyingPointsStandings.tsx b/app/components/scoring/QualifyingPointsStandings.tsx index 89c03d1..e1a1917 100644 --- a/app/components/scoring/QualifyingPointsStandings.tsx +++ b/app/components/scoring/QualifyingPointsStandings.tsx @@ -1,5 +1,6 @@ import { Form } from "react-router"; import { Button } from "~/components/ui/button"; +import { TeamOwnerBadge } from "~/components/ui/team-owner-badge"; import { Card, CardContent, @@ -17,6 +18,13 @@ import { } from "~/components/ui/table"; import { Trophy, CheckCircle2 } from "lucide-react"; +interface TeamOwnership { + participantId: string; + teamName: string; + teamId: string; + ownerName?: string; +} + interface QPStanding { id: string; totalQualifyingPoints: string; @@ -46,6 +54,7 @@ interface QualifyingPointsStandingsProps { totalMajors?: number | null; majorsCompleted?: number; canFinalize: boolean; // Whether all majors are complete + teamOwnerships?: TeamOwnership[]; } export function QualifyingPointsStandings({ @@ -55,53 +64,61 @@ export function QualifyingPointsStandings({ totalMajors, majorsCompleted = 0, canFinalize, + teamOwnerships = [], }: QualifyingPointsStandingsProps) { - // Calculate projected fantasy points for each standing (Q6) - const calculateProjectedPoints = (currentRank: number): number => { - if (!scoringRules || currentRank < 1 || currentRank > 8) return 0; - - const pointsMap: Record = { - 1: scoringRules.pointsFor1st, - 2: scoringRules.pointsFor2nd, - 3: scoringRules.pointsFor3rd, - 4: scoringRules.pointsFor4th, - 5: scoringRules.pointsFor5th, - 6: scoringRules.pointsFor6th, - 7: scoringRules.pointsFor7th, - 8: scoringRules.pointsFor8th, - }; - - return pointsMap[currentRank] || 0; - }; + const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); + const pointsMap: Record = scoringRules + ? { + 1: scoringRules.pointsFor1st, + 2: scoringRules.pointsFor2nd, + 3: scoringRules.pointsFor3rd, + 4: scoringRules.pointsFor4th, + 5: scoringRules.pointsFor5th, + 6: scoringRules.pointsFor6th, + 7: scoringRules.pointsFor7th, + 8: scoringRules.pointsFor8th, + } + : {}; // Assign current ranks with tie handling const rankedStandings: Array = []; - let previousRank = 0; let previousQP = -1; + let previousRankStart = -1; for (let index = 0; index < standings.length; index++) { const standing = standings[index]; const currentQP = parseFloat(standing.totalQualifyingPoints); - // Check if this is a tie with the previous participant let currentRank: number; if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) { - // Same QP as previous = same rank - currentRank = previousRank; + currentRank = previousRankStart; } else { - // Different QP = new rank (1-based position) currentRank = index + 1; } - rankedStandings.push({ - ...standing, - currentRank, - }); + rankedStandings.push({ ...standing, currentRank }); - previousRank = currentRank; + previousRankStart = currentRank; previousQP = currentQP; } + // Pre-compute tied counts per rank to avoid O(N²) scanning per row + const tiedCountByRank = new Map(); + 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 ( @@ -120,7 +137,7 @@ export function QualifyingPointsStandings({ Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed. {scoringRules ? ( - Projected fantasy points show what participants would earn if finalized now. + Projected points account for ties — tied participants share the available points equally. ) : ( @@ -143,6 +160,9 @@ export function QualifyingPointsStandings({ Rank Participant + {teamOwnerships.length > 0 && ( + Owner + )} Total QP Events {scoringRules && !isFinalized && ( @@ -151,15 +171,11 @@ export function QualifyingPointsStandings({ - {rankedStandings.map((standing, idx) => { - const projectedPoints = calculateProjectedPoints(standing.currentRank); + {rankedStandings.map((standing) => { + const projectedPoints = calculateEV(standing.currentRank); const isTop8 = standing.currentRank <= 8; - // Check if this is a tie (same rank as another participant) - const isTied = rankedStandings.some((other, otherIdx) => - otherIdx !== idx && - other.currentRank === standing.currentRank - ); + const isTied = (tiedCountByRank.get(standing.currentRank) ?? 1) > 1; return ( @@ -184,6 +200,21 @@ export function QualifyingPointsStandings({ {standing.participant.name} + {teamOwnerships.length > 0 && ( + + {(() => { + const ownership = ownershipMap.get(standing.participant.id); + return ownership ? ( + + ) : ( + Undrafted + ); + })()} + + )} {parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP @@ -196,7 +227,10 @@ export function QualifyingPointsStandings({ {isTop8 ? ( - {projectedPoints} pts + {projectedPoints % 1 === 0 + ? projectedPoints + : projectedPoints.toFixed(1)}{" "} + pts ) : ( 0 pts diff --git a/app/components/scoring/SportSeasonDisplay.tsx b/app/components/scoring/SportSeasonDisplay.tsx index 6224714..aaed3ab 100644 --- a/app/components/scoring/SportSeasonDisplay.tsx +++ b/app/components/scoring/SportSeasonDisplay.tsx @@ -181,6 +181,7 @@ export function SportSeasonDisplay({ totalMajors={totalMajors} majorsCompleted={majorsCompleted} canFinalize={canFinalize} + teamOwnerships={showOwnership ? teamOwnerships : []} /> );