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 { isCommissioner } from "~/models/commissioner"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { getSocketIO } from "../../../server/socket"; import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { const { request } = args; const { userId } = await getAuth(args); if (!userId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await request.formData(); const seasonId = formData.get("seasonId") as string; const participantId = formData.get("participantId") as string; const pickNumber = parseInt(formData.get("pickNumber") as string); if (!seasonId || !participantId || isNaN(pickNumber) || pickNumber < 1) { return Response.json({ error: "Missing required fields" }, { status: 400 }); } const db = database(); const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season) { return Response.json({ error: "Season not found" }, { status: 404 }); } if (!(await isCommissioner(season.leagueId, userId))) { return Response.json({ error: "Only commissioners can replace picks" }, { status: 403 }); } if (season.status === "pre_draft") { return Response.json({ error: "Draft has not started" }, { status: 400 }); } // Get the existing pick at this slot const existingPick = await db.query.draftPicks.findFirst({ where: and( eq(schema.draftPicks.seasonId, seasonId), eq(schema.draftPicks.pickNumber, pickNumber) ), }); if (!existingPick) { return Response.json({ error: "No pick found at this slot" }, { status: 404 }); } // Use the team from the DB record — don't trust the client-supplied teamId const teamId = existingPick.teamId; const oldParticipantId = existingPick.participantId; // Check new participant isn't already drafted elsewhere (skip if same participant) if (participantId !== oldParticipantId) { const alreadyDrafted = await db.query.draftPicks.findFirst({ where: and( eq(schema.draftPicks.seasonId, seasonId), eq(schema.draftPicks.participantId, participantId) ), }); if (alreadyDrafted) { return Response.json({ error: "Participant already drafted" }, { status: 400 }); } } 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 }); } // Eligibility check — exclude the pick being replaced so its slot is treated as free const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, seasonId), orderBy: schema.draftSlots.draftOrder, with: { team: true }, }); const allPicksWithSports = await getDraftPicksWithSports(seasonId); const teamPicksWithSports = await getTeamDraftPicksWithSports(teamId, seasonId); const allParticipants = await getParticipantsForSeasonWithSports(seasonId); const seasonSports = await getSeasonSportsSimple(seasonId); // Exclude old participant so that slot is "open" for eligibility purposes const allPicksExcluding = allPicksWithSports.filter( (p) => p.participant.id !== oldParticipantId ); const teamPicksExcluding = teamPicksWithSports.filter( (p) => p.participant.id !== oldParticipantId ); const allTeams = draftSlots.map((slot) => ({ id: slot.teamId })); const eligibility = calculateDraftEligibility( teamId, teamPicksExcluding, allPicksExcluding, 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 }); } // Update the pick in-place const [updatedPick] = await db .update(schema.draftPicks) .set({ participantId, pickedByUserId: userId, pickedByType: "commissioner", }) .where( and( eq(schema.draftPicks.seasonId, seasonId), eq(schema.draftPicks.pickNumber, pickNumber) ) ) .returning(); // Remove new participant from all team queues await db .delete(schema.draftQueue) .where( and( eq(schema.draftQueue.seasonId, seasonId), eq(schema.draftQueue.participantId, participantId) ) ); // Emit socket events try { const io = getSocketIO(); const team = draftSlots.find((slot) => slot.team.id === teamId)?.team; io.to(`draft-${seasonId}`).emit("pick-replaced", { seasonId, pickNumber, oldParticipantId, pick: { id: updatedPick.id, pickNumber: updatedPick.pickNumber, round: updatedPick.round, pickInRound: updatedPick.pickInRound, timeUsed: updatedPick.timeUsed, team, participant: { id: participant.id, name: participant.name, sport: participant.sportsSeason.sport, }, sport: participant.sportsSeason.sport, }, }); io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { participantId, }); } catch (error) { logger.error("Socket.IO error:", error); } return Response.json({ success: true }); }