When a participant wins a bracket round, they immediately earn provisional "floor" points (the averaged minimum they'd receive if eliminated next round). These update as they advance and are replaced by finalized scores on elimination. Key changes: - Add `is_partial_score` column to `participant_results` (migration 0038) - `processPlayoffEvent`: assign provisional position 5 to non-scoring round winners; assign round-appropriate floors to scoring round winners via `getGuaranteedMinimumPosition`; add catch-all for unrecognized round names - `upsertParticipantResult`: guard against un-finalizing rows (never overwrite isPartialScore=false with true) - `calculateBracketPoints`: new function averaging tied bracket tiers (5-8 → 20 pts, 3-4 → avg, 1-2 solo); used in `calculateTeamScore` for playoff_bracket pattern (pattern-aware, doesn't affect F1/golf scoring) - `PlayoffBracket`: "In Contention" table for still-active participants; AFL double-chance fix (participants who won a later match excluded from earlier round's loser list); correct `nextRank` starting position - Server loader: batch owner DB queries (one query vs N+1); deduplicate participantPoints; use calculateBracketPoints for bracket point display - Clean up Phase/Q-number tracking comments throughout scoring-calculator.ts - 3 new tests for non-scoring round provisional floor behavior Also includes a dev admin bypass via DEV_ADMIN_CLERK_ID env var (separate change on this branch, not part of floor scoring feature). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
5.3 KiB
TypeScript
206 lines
5.3 KiB
TypeScript
import { PlayoffBracket } from "./PlayoffBracket";
|
|
import { SeasonStandings } from "./SeasonStandings";
|
|
import { QualifyingPointsStandings } from "./QualifyingPointsStandings";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
import { AlertCircle } from "lucide-react";
|
|
|
|
/**
|
|
* SportSeasonDisplay - Pattern detection and display component
|
|
*
|
|
* Detects the scoring pattern of a sports season and displays the appropriate
|
|
* component (PlayoffBracket, SeasonStandings, or QualifyingPointsStandings).
|
|
*
|
|
* This is the main integration point for displaying sport-specific results
|
|
* to league members.
|
|
*/
|
|
|
|
type ScoringPattern =
|
|
| "playoff_bracket"
|
|
| "season_standings"
|
|
| "qualifying_points";
|
|
|
|
interface Match {
|
|
id: string;
|
|
round: string;
|
|
matchNumber: number;
|
|
participant1Id: string | null;
|
|
participant2Id: string | null;
|
|
winnerId: string | null;
|
|
loserId: string | null;
|
|
isComplete: boolean;
|
|
participant1Score: string | null;
|
|
participant2Score: string | null;
|
|
participant1?: {
|
|
id: string;
|
|
name: string;
|
|
} | null;
|
|
participant2?: {
|
|
id: string;
|
|
name: string;
|
|
} | null;
|
|
winner?: {
|
|
id: string;
|
|
name: string;
|
|
} | null;
|
|
loser?: {
|
|
id: string;
|
|
name: string;
|
|
} | null;
|
|
}
|
|
|
|
interface SeasonStanding {
|
|
id: string;
|
|
championshipPoints: string;
|
|
position: number;
|
|
previousPosition?: number | null;
|
|
participant: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
interface QPStanding {
|
|
id: string;
|
|
totalQualifyingPoints: string;
|
|
eventsScored: number;
|
|
finalRanking: number | null;
|
|
participant: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
interface TeamOwnership {
|
|
participantId: string;
|
|
teamName: string;
|
|
teamId: string;
|
|
ownerName?: string;
|
|
}
|
|
|
|
interface ScoringRules {
|
|
pointsFor1st: number;
|
|
pointsFor2nd: number;
|
|
pointsFor3rd: number;
|
|
pointsFor4th: number;
|
|
pointsFor5th: number;
|
|
pointsFor6th: number;
|
|
pointsFor7th: number;
|
|
pointsFor8th: number;
|
|
}
|
|
|
|
interface SportSeasonDisplayProps {
|
|
scoringPattern: ScoringPattern;
|
|
sportSeasonName: string;
|
|
sportName: string;
|
|
|
|
// Playoff data
|
|
playoffMatches?: Match[];
|
|
playoffRounds?: string[];
|
|
preEliminatedParticipants?: { id: string; name: string }[];
|
|
participantPoints?: { participantId: string; points: number }[];
|
|
partialScoreParticipantIds?: string[];
|
|
|
|
// Season standings data (F1)
|
|
seasonStandings?: SeasonStanding[];
|
|
seasonIsFinalized?: boolean;
|
|
|
|
// Qualifying points data (Golf/Tennis)
|
|
qpStandings?: QPStanding[];
|
|
qpIsFinalized?: boolean;
|
|
totalMajors?: number | null;
|
|
majorsCompleted?: number;
|
|
canFinalize?: boolean;
|
|
|
|
// Shared data
|
|
teamOwnerships?: TeamOwnership[];
|
|
userParticipantIds?: string[];
|
|
scoringRules?: ScoringRules | null;
|
|
showOwnership?: boolean;
|
|
}
|
|
|
|
export function SportSeasonDisplay({
|
|
scoringPattern,
|
|
sportSeasonName,
|
|
sportName,
|
|
playoffMatches = [],
|
|
playoffRounds = [],
|
|
preEliminatedParticipants = [],
|
|
participantPoints = [],
|
|
partialScoreParticipantIds = [],
|
|
seasonStandings = [],
|
|
seasonIsFinalized = false,
|
|
qpStandings = [],
|
|
qpIsFinalized = false,
|
|
totalMajors,
|
|
majorsCompleted = 0,
|
|
canFinalize = false,
|
|
teamOwnerships = [],
|
|
userParticipantIds = [],
|
|
scoringRules = null,
|
|
showOwnership = true,
|
|
}: SportSeasonDisplayProps) {
|
|
// Pattern detection and component selection
|
|
switch (scoringPattern) {
|
|
case "playoff_bracket":
|
|
// Display playoff bracket
|
|
return (
|
|
<PlayoffBracket
|
|
matches={playoffMatches}
|
|
rounds={playoffRounds}
|
|
preEliminatedParticipants={preEliminatedParticipants}
|
|
participantPoints={participantPoints}
|
|
partialScoreParticipantIds={partialScoreParticipantIds}
|
|
teamOwnerships={teamOwnerships}
|
|
userParticipantIds={userParticipantIds}
|
|
showOwnership={showOwnership}
|
|
title="Playoff Bracket"
|
|
/>
|
|
);
|
|
|
|
case "season_standings":
|
|
// Display F1-style championship standings
|
|
return (
|
|
<SeasonStandings
|
|
standings={seasonStandings}
|
|
teamOwnerships={teamOwnerships}
|
|
userParticipantIds={userParticipantIds}
|
|
showOwnership={showOwnership}
|
|
isFinalized={seasonIsFinalized}
|
|
title={sportSeasonName}
|
|
description="Championship standings - positions calculated from points"
|
|
/>
|
|
);
|
|
|
|
case "qualifying_points":
|
|
// Display qualifying points standings (Golf/Tennis)
|
|
return (
|
|
<QualifyingPointsStandings
|
|
standings={qpStandings}
|
|
scoringRules={scoringRules}
|
|
isFinalized={qpIsFinalized}
|
|
totalMajors={totalMajors}
|
|
majorsCompleted={majorsCompleted}
|
|
canFinalize={canFinalize}
|
|
/>
|
|
);
|
|
|
|
default:
|
|
// Unknown pattern - show error
|
|
return (
|
|
<Card className="border-destructive/50">
|
|
<CardHeader>
|
|
<CardTitle className="text-destructive flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5" />
|
|
Unknown Scoring Pattern
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
This sports season uses an unsupported scoring pattern: "
|
|
{scoringPattern}". Please contact support.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
}
|