Commissioners can now right-click any picked cell in the draft grid to replace the drafted participant (available during and after draft) or roll back the draft to that pick number (available during draft only). Replace pick enforces sport eligibility with the replaced slot treated as free, updates the pick in-place, and broadcasts a pick-replaced socket event so all clients update in real time. Rollback deletes all picks from the selected pick onward, resets the season pick number, clears all timers, and creates a fresh timer for the team now on the clock. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import { getAuth } from "@clerk/react-router/server";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
import { eq, and, gte } from "drizzle-orm";
|
|
import { getSocketIO } from "../../../server/socket";
|
|
|
|
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 pickNumber = parseInt(formData.get("pickNumber") as string);
|
|
|
|
if (!seasonId || 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 });
|
|
}
|
|
|
|
const isCommissioner = await db.query.commissioners.findFirst({
|
|
where: and(
|
|
eq(schema.commissioners.leagueId, season.leagueId),
|
|
eq(schema.commissioners.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!isCommissioner) {
|
|
return Response.json({ error: "Only commissioners can roll back the draft" }, { status: 403 });
|
|
}
|
|
|
|
if (season.status !== "draft") {
|
|
return Response.json(
|
|
{ error: "Cannot roll back a draft that is not in progress" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
|
orderBy: schema.draftSlots.draftOrder,
|
|
});
|
|
|
|
const totalTeams = draftSlots.length;
|
|
|
|
// Calculate which team is on the clock at the rollback pick (snake draft)
|
|
const round = Math.ceil(pickNumber / totalTeams);
|
|
const isEvenRound = round % 2 === 0;
|
|
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
|
if (isEvenRound) {
|
|
pickInRound = totalTeams - pickInRound + 1;
|
|
}
|
|
const rollbackSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound);
|
|
|
|
// Delete picks from pickNumber onwards
|
|
await db
|
|
.delete(schema.draftPicks)
|
|
.where(
|
|
and(
|
|
eq(schema.draftPicks.seasonId, seasonId),
|
|
gte(schema.draftPicks.pickNumber, pickNumber)
|
|
)
|
|
);
|
|
|
|
// Clear all timers and create a fresh one for the team now on the clock
|
|
await db
|
|
.delete(schema.draftTimers)
|
|
.where(eq(schema.draftTimers.seasonId, seasonId));
|
|
|
|
const initialTime = season.draftInitialTime || 120;
|
|
|
|
if (rollbackSlot) {
|
|
await db.insert(schema.draftTimers).values({
|
|
seasonId,
|
|
teamId: rollbackSlot.teamId,
|
|
timeRemaining: initialTime,
|
|
});
|
|
}
|
|
|
|
// Update season: reset pick number and unpause
|
|
await db
|
|
.update(schema.seasons)
|
|
.set({
|
|
currentPickNumber: pickNumber,
|
|
draftPaused: false,
|
|
})
|
|
.where(eq(schema.seasons.id, seasonId));
|
|
|
|
// Emit socket events
|
|
try {
|
|
const io = getSocketIO();
|
|
|
|
io.to(`draft-${seasonId}`).emit("draft-rolled-back", {
|
|
seasonId,
|
|
pickNumber,
|
|
teamId: rollbackSlot?.teamId,
|
|
});
|
|
|
|
if (rollbackSlot) {
|
|
io.to(`draft-${seasonId}`).emit("timer-update", {
|
|
seasonId,
|
|
teamId: rollbackSlot.teamId,
|
|
timeRemaining: initialTime,
|
|
currentPickNumber: pickNumber,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Socket.IO error:", error);
|
|
}
|
|
|
|
return Response.json({ success: true, pickNumber });
|
|
}
|