brackt/app/routes/api/draft.force-manual-pick.ts
Chris Parsons decb28dc19 Implement Omni League Draft Eligibility Rules
- 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
2025-10-24 21:12:07 -07:00

221 lines
6.4 KiB
TypeScript

import { getAuth } from "@clerk/react-router/server";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick";
import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
export async function action(args: any) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await request.formData();
const seasonId = formData.get("seasonId") as string;
const teamId = formData.get("teamId") as string;
const participantId = formData.get("participantId") as string;
const pickNumber = parseInt(formData.get("pickNumber") as string);
if (!seasonId || !teamId || !participantId || !pickNumber) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
const db = database();
// Get season details
const season = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
with: {
league: true,
},
});
if (!season) {
return Response.json({ error: "Season not found" }, { status: 404 });
}
// Check if user is commissioner
if (season.league.createdBy !== userId) {
return Response.json({ error: "Only commissioner can force manual pick" }, { status: 403 });
}
// Check if participant is already drafted
const existingPick = await db.query.draftPicks.findFirst({
where: and(
eq(schema.draftPicks.seasonId, seasonId),
eq(schema.draftPicks.participantId, participantId)
),
});
if (existingPick) {
return Response.json({ error: "Participant already drafted" }, { status: 400 });
}
// Get participant details
const participant = await db.query.participants.findFirst({
where: eq(schema.participants.id, participantId),
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
});
if (!participant) {
return Response.json({ error: "Participant not found" }, { status: 404 });
}
// Calculate round and pickInRound
const draftSlots = await db.query.draftSlots.findMany({
where: eq(schema.draftSlots.seasonId, seasonId),
orderBy: schema.draftSlots.draftOrder,
with: {
team: true,
},
});
const totalTeams = draftSlots.length;
const currentRound = Math.ceil(pickNumber / totalTeams);
const isEvenRound = currentRound % 2 === 0;
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
if (isEvenRound) {
pickInRound = totalTeams - pickInRound + 1;
}
// ELIGIBILITY VALIDATION: Check if team can draft from this sport
const allPicks = await getDraftPicksWithSports(seasonId);
const teamPicks = await getTeamDraftPicksWithSports(teamId, seasonId);
const allParticipants = await getParticipantsForSeasonWithSports(seasonId);
const seasonSports = await getSeasonSportsSimple(seasonId);
// Get all teams for the season
const allTeams = draftSlots.map((slot) => ({ id: slot.teamId }));
const eligibility = calculateDraftEligibility(
teamId,
teamPicks,
allPicks,
allParticipants,
seasonSports,
season.draftRounds,
allTeams
);
const sportId = participant.sportsSeason.sport.id;
if (!eligibility.eligibleSportIds.has(sportId)) {
const reason = eligibility.ineligibleReasons[sportId] || "Cannot draft from this sport";
return Response.json({ error: reason }, { status: 400 });
}
// Create the draft pick
const [draftPick] = await db
.insert(schema.draftPicks)
.values({
seasonId,
teamId,
participantId,
pickNumber,
round: currentRound,
pickInRound,
pickedByUserId: userId,
pickedByType: "commissioner",
})
.returning();
// Update season's current pick number
const nextPickNumber = pickNumber + 1;
const totalPicks = totalTeams * season.draftRounds;
const isDraftComplete = nextPickNumber > totalPicks;
await db
.update(schema.seasons)
.set({
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status,
})
.where(eq(schema.seasons.id, seasonId));
// Add increment to the team that just picked
const currentTimer = await db.query.draftTimers.findFirst({
where: and(
eq(schema.draftTimers.seasonId, seasonId),
eq(schema.draftTimers.teamId, teamId)
),
});
if (currentTimer) {
const incrementTime = season.draftIncrementTime || 30;
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
await db
.update(schema.draftTimers)
.set({
timeRemaining: newTimeRemaining,
updatedAt: new Date(),
})
.where(eq(schema.draftTimers.id, currentTimer.id));
// Emit timer update to all clients
try {
const io = (global as any).__socketIO;
io.to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId,
timeRemaining: newTimeRemaining,
currentPickNumber: pickNumber,
});
} catch (error) {
console.error("Socket.IO timer-update error:", error);
}
}
// Remove from ALL team queues in this season (participant is now drafted)
await db
.delete(schema.draftQueue)
.where(
and(
eq(schema.draftQueue.seasonId, seasonId),
eq(schema.draftQueue.participantId, participantId)
)
);
// Emit socket event
try {
const io = (global as any).__socketIO;
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team;
io.to(`draft-${seasonId}`).emit("pick-made", {
pick: {
...draftPick,
team,
participant: {
...participant,
sport: participant.sportsSeason.sport,
},
sport: participant.sportsSeason.sport,
},
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
isDraftComplete,
});
if (isDraftComplete) {
io.to(`draft-${seasonId}`).emit("draft-completed");
}
} catch (error) {
console.error("Socket.IO error:", error);
}
return Response.json({
success: true,
pick: draftPick,
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
isDraftComplete,
});
}