2025-11-11 10:08:25 -08:00
|
|
|
import { Form } from "react-router";
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from "~/components/ui/card";
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from "~/components/ui/table";
|
|
|
|
|
import { Trophy, CheckCircle2 } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
interface QPStanding {
|
|
|
|
|
id: string;
|
|
|
|
|
totalQualifyingPoints: string;
|
|
|
|
|
eventsScored: number;
|
|
|
|
|
finalRanking: number | null;
|
|
|
|
|
participant: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ScoringRules {
|
|
|
|
|
pointsFor1st: number;
|
|
|
|
|
pointsFor2nd: number;
|
|
|
|
|
pointsFor3rd: number;
|
|
|
|
|
pointsFor4th: number;
|
|
|
|
|
pointsFor5th: number;
|
|
|
|
|
pointsFor6th: number;
|
|
|
|
|
pointsFor7th: number;
|
|
|
|
|
pointsFor8th: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface QualifyingPointsStandingsProps {
|
|
|
|
|
standings: QPStanding[];
|
|
|
|
|
scoringRules: ScoringRules | null;
|
|
|
|
|
isFinalized: boolean;
|
|
|
|
|
totalMajors?: number | null;
|
|
|
|
|
majorsCompleted?: number;
|
|
|
|
|
canFinalize: boolean; // Whether all majors are complete
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function QualifyingPointsStandings({
|
|
|
|
|
standings,
|
|
|
|
|
scoringRules,
|
|
|
|
|
isFinalized,
|
|
|
|
|
totalMajors,
|
|
|
|
|
majorsCompleted = 0,
|
|
|
|
|
canFinalize,
|
|
|
|
|
}: 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<number, number> = {
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Assign current ranks with tie handling
|
|
|
|
|
const rankedStandings: Array<typeof standings[0] & { currentRank: number }> = [];
|
|
|
|
|
let previousRank = 0;
|
|
|
|
|
let previousQP = -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;
|
|
|
|
|
} else {
|
|
|
|
|
// Different QP = new rank (1-based position)
|
|
|
|
|
currentRank = index + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rankedStandings.push({
|
|
|
|
|
...standing,
|
|
|
|
|
currentRank,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
previousRank = currentRank;
|
|
|
|
|
previousQP = currentQP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-20 19:26:11 -08:00
|
|
|
<Card className={isFinalized ? "border-emerald-500/30" : "border-amber-accent/30"}>
|
2025-11-11 10:08:25 -08:00
|
|
|
<CardHeader>
|
2026-02-20 19:26:11 -08:00
|
|
|
<CardTitle className={isFinalized ? "text-emerald-400" : "text-amber-accent"}>
|
2025-11-11 10:08:25 -08:00
|
|
|
<Trophy className="inline mr-2 h-5 w-5" />
|
|
|
|
|
Qualifying Points Standings
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{isFinalized ? (
|
2026-02-20 19:26:11 -08:00
|
|
|
<span className="text-emerald-400 font-semibold">
|
2025-11-11 10:08:25 -08:00
|
|
|
<CheckCircle2 className="inline h-4 w-4 mr-1" />
|
|
|
|
|
Finalized - Fantasy points have been assigned
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
Current standings based on {majorsCompleted} of {totalMajors || '?'} major tournaments completed.
|
|
|
|
|
{scoringRules ? (
|
|
|
|
|
<span className="block mt-1">
|
|
|
|
|
Projected fantasy points show what participants would earn if finalized now.
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="block mt-1 text-muted-foreground">
|
|
|
|
|
Projected points will be shown when linked to a fantasy season.
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{standings.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
No qualifying points awarded yet. Complete a qualifying event to see standings.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead className="w-20">Rank</TableHead>
|
|
|
|
|
<TableHead>Participant</TableHead>
|
|
|
|
|
<TableHead className="w-28 text-right">Total QP</TableHead>
|
|
|
|
|
<TableHead className="w-24 text-right">Events</TableHead>
|
|
|
|
|
{scoringRules && !isFinalized && (
|
|
|
|
|
<TableHead className="w-32 text-right">Projected Points</TableHead>
|
|
|
|
|
)}
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{rankedStandings.map((standing, idx) => {
|
|
|
|
|
const projectedPoints = calculateProjectedPoints(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
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TableRow key={standing.id} className={!isTop8 ? "opacity-60" : ""}>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{standing.currentRank === 1 && !isTied && <span className="text-xl">🥇</span>}
|
|
|
|
|
{standing.currentRank === 2 && !isTied && <span className="text-xl">🥈</span>}
|
|
|
|
|
{standing.currentRank === 3 && !isTied && <span className="text-xl">🥉</span>}
|
|
|
|
|
<span className="font-semibold">
|
|
|
|
|
{isTied && "T"}
|
|
|
|
|
{standing.currentRank}
|
|
|
|
|
{standing.currentRank === 1
|
|
|
|
|
? "st"
|
|
|
|
|
: standing.currentRank === 2
|
|
|
|
|
? "nd"
|
|
|
|
|
: standing.currentRank === 3
|
|
|
|
|
? "rd"
|
|
|
|
|
: "th"}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="font-medium">
|
|
|
|
|
{standing.participant.name}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
2026-02-20 19:26:11 -08:00
|
|
|
<span className="font-semibold text-amber-accent">
|
2025-11-11 10:08:25 -08:00
|
|
|
{parseFloat(standing.totalQualifyingPoints).toFixed(2)} QP
|
|
|
|
|
</span>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right text-muted-foreground">
|
|
|
|
|
{standing.eventsScored}
|
|
|
|
|
</TableCell>
|
|
|
|
|
{scoringRules && !isFinalized && (
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
{isTop8 ? (
|
2026-02-20 19:26:11 -08:00
|
|
|
<span className="font-semibold text-emerald-400">
|
2025-11-11 10:08:25 -08:00
|
|
|
{projectedPoints} pts
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">0 pts</span>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
)}
|
|
|
|
|
</TableRow>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
|
|
|
|
|
{!isFinalized && canFinalize && (
|
|
|
|
|
<div className="mt-6 p-4 border-t">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-sm mb-1">Ready to finalize?</h4>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
All {totalMajors} major tournaments are complete. Finalizing will:
|
|
|
|
|
</p>
|
|
|
|
|
<ul className="list-disc list-inside text-sm text-muted-foreground mt-2 space-y-1">
|
|
|
|
|
<li>Convert the top 8 participants to fantasy placements 1-8</li>
|
|
|
|
|
<li>Award fantasy points based on league scoring rules</li>
|
|
|
|
|
<li>Update all league standings</li>
|
|
|
|
|
<li>Lock the qualifying points (no further changes)</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
<Form method="post">
|
|
|
|
|
<input type="hidden" name="intent" value="finalize-qp" />
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="bg-amber-600 hover:bg-amber-700"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (!confirm("Are you sure you want to finalize qualifying points? This action cannot be undone.")) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
|
|
|
Finalize Qualifying Points & Assign Fantasy Points
|
|
|
|
|
</Button>
|
|
|
|
|
</Form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!isFinalized && !canFinalize && (
|
|
|
|
|
<div className="mt-6 p-4 border-t">
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
<span className="font-semibold">Not ready to finalize yet.</span> Complete all {totalMajors} major tournaments before finalizing qualifying points.
|
|
|
|
|
({majorsCompleted} of {totalMajors} completed)
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|