- Created draft eligibility calculation logic in `app/lib/draft-eligibility.ts` - Added interfaces for sport availability and draft eligibility - Implemented `calculateDraftEligibility` function to determine eligible sports based on team picks and global availability - Developed `getEligibilitySummary` function for human-readable eligibility status - Updated API endpoints to validate eligibility before making picks - Enhanced frontend draft room UI to reflect eligibility status and provide visual indicators for ineligible participants - Comprehensive implementation summary and testing documentation added - All changes type-checked and unit tests passed successfully
195 lines
5.5 KiB
TypeScript
195 lines
5.5 KiB
TypeScript
/**
|
|
* Draft Eligibility Calculation for Omni Leagues
|
|
*
|
|
* Implements two key constraints:
|
|
* 1. Team Flex Pick Limit: Teams can only use (totalRounds - totalSports) flex picks
|
|
* 2. Global Sport Availability: Cannot draft from a sport if it prevents other teams
|
|
* from fulfilling their "at least one from each sport" requirement
|
|
*/
|
|
|
|
export interface SportAvailability {
|
|
sportId: string;
|
|
sportName: string;
|
|
totalParticipants: number;
|
|
draftedCount: number;
|
|
undraftedCount: number;
|
|
teamsStillNeeding: number;
|
|
canDraftAsFirst: boolean; // undraftedCount >= teamsStillNeeding
|
|
canDraftAsFlex: boolean; // undraftedCount > teamsStillNeeding
|
|
}
|
|
|
|
export interface DraftEligibility {
|
|
teamId: string;
|
|
eligibleSportIds: Set<string>;
|
|
ineligibleReasons: Record<string, string>; // sportId -> reason
|
|
flexPicksUsed: number;
|
|
flexPicksAvailable: number;
|
|
picksBySport: Record<string, number>;
|
|
sportAvailability: Record<string, SportAvailability>;
|
|
}
|
|
|
|
interface PickData {
|
|
teamId: string;
|
|
participant: {
|
|
id: string;
|
|
sport: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
interface ParticipantData {
|
|
id: string;
|
|
sport: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
interface SportData {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface TeamData {
|
|
id: string;
|
|
}
|
|
|
|
/**
|
|
* Calculate which sports a team is eligible to draft from based on:
|
|
* - Team's flex pick usage
|
|
* - Global sport availability for all teams
|
|
*/
|
|
export function calculateDraftEligibility(
|
|
teamId: string,
|
|
teamPicks: PickData[],
|
|
allPicks: PickData[],
|
|
allParticipants: ParticipantData[],
|
|
seasonSports: SportData[],
|
|
totalRounds: number,
|
|
allTeams: TeamData[]
|
|
): DraftEligibility {
|
|
// Step 1: Calculate team's flex pick status
|
|
const picksBySport: Record<string, number> = {};
|
|
|
|
for (const pick of teamPicks) {
|
|
const sportId = pick.participant.sport.id;
|
|
picksBySport[sportId] = (picksBySport[sportId] || 0) + 1;
|
|
}
|
|
|
|
// Calculate flex picks used: sum of (picks - 1) for each sport with multiple picks
|
|
let flexPicksUsed = 0;
|
|
for (const sportId in picksBySport) {
|
|
if (picksBySport[sportId] > 1) {
|
|
flexPicksUsed += picksBySport[sportId] - 1;
|
|
}
|
|
}
|
|
|
|
const totalSports = seasonSports.length;
|
|
const flexPicksAvailable = totalRounds - totalSports;
|
|
const hasFlexesRemaining = flexPicksUsed < flexPicksAvailable;
|
|
|
|
// Step 2: Calculate global sport availability
|
|
const sportAvailability: Record<string, SportAvailability> = {};
|
|
|
|
for (const sport of seasonSports) {
|
|
// Count total participants in this sport
|
|
const totalParticipants = allParticipants.filter(
|
|
p => p.sport.id === sport.id
|
|
).length;
|
|
|
|
// Count drafted participants in this sport
|
|
const draftedCount = allPicks.filter(
|
|
pick => pick.participant.sport.id === sport.id
|
|
).length;
|
|
|
|
const undraftedCount = totalParticipants - draftedCount;
|
|
|
|
// Count teams that haven't drafted from this sport yet
|
|
const teamsSportPicks: Record<string, number> = {};
|
|
for (const pick of allPicks) {
|
|
if (pick.participant.sport.id === sport.id) {
|
|
teamsSportPicks[pick.teamId] = (teamsSportPicks[pick.teamId] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
const teamsStillNeeding = allTeams.filter(
|
|
team => !teamsSportPicks[team.id] || teamsSportPicks[team.id] === 0
|
|
).length;
|
|
|
|
sportAvailability[sport.id] = {
|
|
sportId: sport.id,
|
|
sportName: sport.name,
|
|
totalParticipants,
|
|
draftedCount,
|
|
undraftedCount,
|
|
teamsStillNeeding,
|
|
canDraftAsFirst: undraftedCount >= teamsStillNeeding,
|
|
canDraftAsFlex: undraftedCount > teamsStillNeeding,
|
|
};
|
|
}
|
|
|
|
// Step 3: Combine constraints to determine eligibility
|
|
const eligibleSportIds = new Set<string>();
|
|
const ineligibleReasons: Record<string, string> = {};
|
|
|
|
for (const sport of seasonSports) {
|
|
const sportId = sport.id;
|
|
const availability = sportAvailability[sportId];
|
|
const currentTeamHasSport = (picksBySport[sportId] || 0) > 0;
|
|
|
|
let isEligible = false;
|
|
let reason = "";
|
|
|
|
if (!currentTeamHasSport) {
|
|
// Team hasn't drafted from this sport yet (would be first pick)
|
|
if (availability.canDraftAsFirst) {
|
|
isEligible = true;
|
|
} else {
|
|
reason = `Only ${availability.undraftedCount} participants left for ${availability.teamsStillNeeding} teams needing ${sport.name}`;
|
|
}
|
|
} else {
|
|
// Team has drafted from this sport (would be flex pick)
|
|
if (!hasFlexesRemaining) {
|
|
reason = `All flex picks used (${flexPicksUsed}/${flexPicksAvailable})`;
|
|
} else if (!availability.canDraftAsFlex) {
|
|
reason = `Would prevent other teams from getting ${sport.name} (${availability.undraftedCount} left for ${availability.teamsStillNeeding} teams)`;
|
|
} else {
|
|
isEligible = true;
|
|
}
|
|
}
|
|
|
|
if (isEligible) {
|
|
eligibleSportIds.add(sportId);
|
|
} else {
|
|
ineligibleReasons[sportId] = reason;
|
|
}
|
|
}
|
|
|
|
return {
|
|
teamId,
|
|
eligibleSportIds,
|
|
ineligibleReasons,
|
|
flexPicksUsed,
|
|
flexPicksAvailable,
|
|
picksBySport,
|
|
sportAvailability,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a human-readable summary of draft eligibility status
|
|
*/
|
|
export function getEligibilitySummary(eligibility: DraftEligibility): string {
|
|
const { flexPicksUsed, flexPicksAvailable, picksBySport, sportAvailability } = eligibility;
|
|
|
|
const sportBreakdown = Object.entries(picksBySport)
|
|
.map(([sportId, count]) => {
|
|
const sport = sportAvailability[sportId];
|
|
return `${sport?.sportName || sportId}: ${count}`;
|
|
})
|
|
.join(", ");
|
|
|
|
return `Flex picks: ${flexPicksUsed}/${flexPicksAvailable} | Picks by sport: ${sportBreakdown || "None"}`;
|
|
}
|