feat: add draft increment time and show team-specific timers in draft UI
This commit is contained in:
parent
fa5fb6d06e
commit
a7f5df923f
6 changed files with 571 additions and 25 deletions
|
|
@ -169,18 +169,49 @@ export async function action(args: any) {
|
||||||
})
|
})
|
||||||
.where(eq(schema.seasons.id, seasonId));
|
.where(eq(schema.seasons.id, seasonId));
|
||||||
|
|
||||||
// Remove from queue if it was picked from queue
|
// Add increment to the team that just picked
|
||||||
if (teamQueue.some((item) => item.participantId === participantToPick.id)) {
|
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
|
await db
|
||||||
.delete(schema.draftQueue)
|
.update(schema.draftTimers)
|
||||||
.where(
|
.set({
|
||||||
and(
|
timeRemaining: newTimeRemaining,
|
||||||
eq(schema.draftQueue.teamId, teamId),
|
updatedAt: new Date(),
|
||||||
eq(schema.draftQueue.participantId, participantToPick.id)
|
})
|
||||||
)
|
.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, participantToPick.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Emit socket event
|
// Emit socket event
|
||||||
try {
|
try {
|
||||||
const io = (global as any).__socketIO;
|
const io = (global as any).__socketIO;
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,49 @@ export async function action(args: any) {
|
||||||
})
|
})
|
||||||
.where(eq(schema.seasons.id, seasonId));
|
.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
|
// Emit socket event
|
||||||
try {
|
try {
|
||||||
const io = (global as any).__socketIO;
|
const io = (global as any).__socketIO;
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,49 @@ export async function action(args: any) {
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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, currentDraftSlot.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: currentDraftSlot.teamId,
|
||||||
|
timeRemaining: newTimeRemaining,
|
||||||
|
currentPickNumber,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Socket.IO timer-update error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update season's current pick number
|
// Update season's current pick number
|
||||||
const nextPickNumber = currentPickNumber + 1;
|
const nextPickNumber = currentPickNumber + 1;
|
||||||
const totalPicks = totalTeams * season.draftRounds;
|
const totalPicks = totalTeams * season.draftRounds;
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,11 @@ export async function loader(args: any) {
|
||||||
// Load user's team queue if they have a team
|
// Load user's team queue if they have a team
|
||||||
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
|
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
|
||||||
|
|
||||||
|
// Load draft timers for all teams
|
||||||
|
const timers = await db.query.draftTimers.findMany({
|
||||||
|
where: eq(schema.draftTimers.seasonId, seasonId),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
season,
|
season,
|
||||||
draftSlots,
|
draftSlots,
|
||||||
|
|
@ -152,6 +157,7 @@ export async function loader(args: any) {
|
||||||
availableParticipants,
|
availableParticipants,
|
||||||
userTeam,
|
userTeam,
|
||||||
userQueue,
|
userQueue,
|
||||||
|
timers,
|
||||||
isCommissioner: !!isCommissioner,
|
isCommissioner: !!isCommissioner,
|
||||||
currentUserId: userId,
|
currentUserId: userId,
|
||||||
};
|
};
|
||||||
|
|
@ -165,7 +171,9 @@ export default function DraftRoom() {
|
||||||
availableParticipants,
|
availableParticipants,
|
||||||
userTeam,
|
userTeam,
|
||||||
userQueue,
|
userQueue,
|
||||||
|
timers,
|
||||||
isCommissioner,
|
isCommissioner,
|
||||||
|
currentUserId,
|
||||||
} = useLoaderData<typeof loader>();
|
} = useLoaderData<typeof loader>();
|
||||||
const { isConnected, on, off } = useDraftSocket(season.id);
|
const { isConnected, on, off } = useDraftSocket(season.id);
|
||||||
const [picks, setPicks] = useState(draftPicks);
|
const [picks, setPicks] = useState(draftPicks);
|
||||||
|
|
@ -175,6 +183,14 @@ export default function DraftRoom() {
|
||||||
const [sportFilter, setSportFilter] = useState<string>("all");
|
const [sportFilter, setSportFilter] = useState<string>("all");
|
||||||
const [queue, setQueue] = useState(userQueue);
|
const [queue, setQueue] = useState(userQueue);
|
||||||
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
|
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
|
||||||
|
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
|
||||||
|
// Initialize with timers from loader data
|
||||||
|
const timersMap: Record<string, number> = {};
|
||||||
|
timers.forEach((timer: any) => {
|
||||||
|
timersMap[timer.teamId] = timer.timeRemaining;
|
||||||
|
});
|
||||||
|
return timersMap;
|
||||||
|
});
|
||||||
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
|
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
|
||||||
const [selectedPickSlot, setSelectedPickSlot] = useState<{
|
const [selectedPickSlot, setSelectedPickSlot] = useState<{
|
||||||
pickNumber: number;
|
pickNumber: number;
|
||||||
|
|
@ -190,6 +206,13 @@ export default function DraftRoom() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimerUpdate = (data: any) => {
|
const handleTimerUpdate = (data: any) => {
|
||||||
|
// Update the specific team's timer
|
||||||
|
setTeamTimers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[data.teamId]: data.timeRemaining,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Also update current pick timer for backward compatibility
|
||||||
if (data.currentPickNumber === currentPick) {
|
if (data.currentPickNumber === currentPick) {
|
||||||
setTimeRemaining(data.timeRemaining);
|
setTimeRemaining(data.timeRemaining);
|
||||||
}
|
}
|
||||||
|
|
@ -464,13 +487,6 @@ export default function DraftRoom() {
|
||||||
<span className="capitalize">
|
<span className="capitalize">
|
||||||
{season.status.replace("_", " ")}
|
{season.status.replace("_", " ")}
|
||||||
</span>
|
</span>
|
||||||
{season.status === "draft" && (
|
|
||||||
<span
|
|
||||||
className={`font-mono font-semibold ${getTimerColor(timeRemaining)}`}
|
|
||||||
>
|
|
||||||
⏱ {formatTime(timeRemaining)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -507,16 +523,19 @@ export default function DraftRoom() {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Team Headers */}
|
{/* Team Headers */}
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
{draftSlots.map((slot) => (
|
{draftSlots.map((slot) => {
|
||||||
<div key={slot.id} className="flex-1 min-w-32 text-center">
|
const teamTime = teamTimers[slot.team.id];
|
||||||
<div className="font-semibold text-sm truncate px-2">
|
return (
|
||||||
{slot.team.name}
|
<div key={slot.id} className="flex-1 min-w-32 text-center">
|
||||||
|
<div className="font-semibold text-sm truncate px-2">
|
||||||
|
{slot.team.name}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs font-mono ${getTimerColor(teamTime)}`}>
|
||||||
|
{formatTime(teamTime)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
);
|
||||||
Pick {slot.draftOrder}
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Draft Grid Rows */}
|
{/* Draft Grid Rows */}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,14 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||||
global.__socketIO = io;
|
global.__socketIO = io;
|
||||||
|
|
||||||
console.log("Socket.IO initialized");
|
console.log("Socket.IO initialized");
|
||||||
|
|
||||||
|
// Start the draft timer system (async import but don't await)
|
||||||
|
import("./timer").then(({ startDraftTimerSystem }) => {
|
||||||
|
startDraftTimerSystem();
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to start timer system:", error);
|
||||||
|
});
|
||||||
|
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
402
server/timer.ts
Normal file
402
server/timer.ts
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import * as schema from "../database/schema";
|
||||||
|
import { eq, and, desc, asc, inArray, notInArray } from "drizzle-orm";
|
||||||
|
import { getSocketIO } from "./socket";
|
||||||
|
|
||||||
|
// Create a dedicated database connection for the timer
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("DATABASE_URL is required for timer system");
|
||||||
|
}
|
||||||
|
const client = postgres(connectionString);
|
||||||
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
|
let timerInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the draft timer system
|
||||||
|
* Runs every second to update all active draft timers
|
||||||
|
*/
|
||||||
|
export function startDraftTimerSystem(): void {
|
||||||
|
if (timerInterval) {
|
||||||
|
console.log("[Timer] Timer system already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timerInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await updateDraftTimers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Timer] Error updating draft timers:", error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
console.log("[Timer] Draft timer system started");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the draft timer system
|
||||||
|
*/
|
||||||
|
export function stopDraftTimerSystem(): void {
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
timerInterval = null;
|
||||||
|
console.log("[Timer] Draft timer system stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all active draft timers
|
||||||
|
* Called every second by the timer interval
|
||||||
|
*/
|
||||||
|
async function updateDraftTimers(): Promise<void> {
|
||||||
|
const io = getSocketIO();
|
||||||
|
|
||||||
|
// Get all active drafts
|
||||||
|
const activeDrafts = await db.query.seasons.findMany({
|
||||||
|
where: eq(schema.seasons.status, "draft"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeDrafts.length === 0) {
|
||||||
|
return; // No active drafts
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Timer] Updating timers for ${activeDrafts.length} active draft(s)`);
|
||||||
|
|
||||||
|
for (const season of activeDrafts) {
|
||||||
|
// Skip if draft is paused
|
||||||
|
if (season.draftPaused) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPickNumber = season.currentPickNumber || 1;
|
||||||
|
|
||||||
|
// Get draft slots to determine whose turn it is
|
||||||
|
const draftSlots = await db.query.draftSlots.findMany({
|
||||||
|
where: eq(schema.draftSlots.seasonId, season.id),
|
||||||
|
orderBy: asc(schema.draftSlots.draftOrder),
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalTeams = draftSlots.length;
|
||||||
|
if (totalTeams === 0) continue;
|
||||||
|
|
||||||
|
// Calculate current round and pick
|
||||||
|
const currentRound = Math.ceil(currentPickNumber / totalTeams);
|
||||||
|
const isEvenRound = currentRound % 2 === 0;
|
||||||
|
let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1;
|
||||||
|
|
||||||
|
// Snake draft: reverse order on even rounds
|
||||||
|
if (isEvenRound) {
|
||||||
|
pickInRound = totalTeams - pickInRound + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDraftSlot = draftSlots.find(
|
||||||
|
(slot: any) => slot.draftOrder === pickInRound
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentDraftSlot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTeamId = currentDraftSlot.teamId;
|
||||||
|
|
||||||
|
// Get current team's timer
|
||||||
|
const timer = await db.query.draftTimers.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.draftTimers.seasonId, season.id),
|
||||||
|
eq(schema.draftTimers.teamId, currentTeamId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!timer) {
|
||||||
|
console.warn(`[Timer] No timer found for team ${currentTeamId} in season ${season.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If timer is at 0 or below, trigger auto-pick immediately
|
||||||
|
if (timer.timeRemaining <= 0) {
|
||||||
|
console.log(
|
||||||
|
`[Timer] Timer at 0 for team ${currentTeamId} in season ${season.id}, triggering auto-pick`
|
||||||
|
);
|
||||||
|
await triggerAutoPick(season.id, currentTeamId, currentPickNumber);
|
||||||
|
continue; // Skip to next draft after triggering pick
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement timer
|
||||||
|
const newTimeRemaining = timer.timeRemaining - 1;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Timer] Season ${season.id}: Team ${currentTeamId} - ${newTimeRemaining}s remaining (pick ${currentPickNumber})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update timer in database
|
||||||
|
await db
|
||||||
|
.update(schema.draftTimers)
|
||||||
|
.set({
|
||||||
|
timeRemaining: newTimeRemaining,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.draftTimers.id, timer.id));
|
||||||
|
|
||||||
|
// Emit timer update to all clients in the draft room
|
||||||
|
io.to(`draft-${season.id}`).emit("timer-update", {
|
||||||
|
seasonId: season.id,
|
||||||
|
teamId: currentTeamId,
|
||||||
|
timeRemaining: newTimeRemaining,
|
||||||
|
currentPickNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If timer just hit 0, trigger auto-pick
|
||||||
|
if (newTimeRemaining === 0) {
|
||||||
|
console.log(
|
||||||
|
`[Timer] Timer expired for team ${currentTeamId} in season ${season.id}, triggering auto-pick`
|
||||||
|
);
|
||||||
|
await triggerAutoPick(season.id, currentTeamId, currentPickNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger an automatic pick when timer expires
|
||||||
|
*/
|
||||||
|
async function triggerAutoPick(
|
||||||
|
seasonId: string,
|
||||||
|
teamId: string,
|
||||||
|
pickNumber: number
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const io = getSocketIO();
|
||||||
|
|
||||||
|
// Get season details
|
||||||
|
const season = await db.query.seasons.findFirst({
|
||||||
|
where: eq(schema.seasons.id, seasonId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!season) {
|
||||||
|
console.error(`[Timer] Season ${seasonId} not found for auto-pick`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pick already made (race condition protection)
|
||||||
|
const existingPick = await db.query.draftPicks.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.draftPicks.seasonId, seasonId),
|
||||||
|
eq(schema.draftPicks.pickNumber, pickNumber)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPick) {
|
||||||
|
console.log(`[Timer] Pick ${pickNumber} already made, skipping auto-pick`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get team's queue
|
||||||
|
const queueItems = await db.query.draftQueue.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(schema.draftQueue.seasonId, seasonId),
|
||||||
|
eq(schema.draftQueue.teamId, teamId)
|
||||||
|
),
|
||||||
|
orderBy: asc(schema.draftQueue.queuePosition),
|
||||||
|
});
|
||||||
|
|
||||||
|
let participantId: string | null = null;
|
||||||
|
|
||||||
|
// Try to pick from queue first
|
||||||
|
if (queueItems.length > 0) {
|
||||||
|
// Get already drafted participant IDs
|
||||||
|
const draftedPicks = await db.query.draftPicks.findMany({
|
||||||
|
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||||
|
});
|
||||||
|
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||||||
|
|
||||||
|
// Find first queued participant that hasn't been drafted
|
||||||
|
const availableQueueItem = queueItems.find(
|
||||||
|
(item: any) => !draftedParticipantIds.includes(item.participantId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableQueueItem) {
|
||||||
|
participantId = availableQueueItem.participantId;
|
||||||
|
console.log(`[Timer] Auto-picking from queue: ${participantId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid queue item, pick highest EV available participant
|
||||||
|
if (!participantId) {
|
||||||
|
// Get sports season IDs for this season
|
||||||
|
const seasonSports = await db.query.seasonTemplateSports.findMany({
|
||||||
|
where: eq(schema.seasonTemplateSports.templateId, season.templateId!),
|
||||||
|
});
|
||||||
|
const sportsSeasonIds = seasonSports.map((ss: any) => ss.sportsSeasonId);
|
||||||
|
|
||||||
|
if (sportsSeasonIds.length === 0) {
|
||||||
|
console.error(`[Timer] No sports seasons found for season ${seasonId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get already drafted participant IDs
|
||||||
|
const draftedPicks = await db.query.draftPicks.findMany({
|
||||||
|
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||||
|
});
|
||||||
|
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||||||
|
|
||||||
|
// Query available participants from the sports seasons
|
||||||
|
const availableParticipants = await db.query.participants.findMany({
|
||||||
|
where: and(
|
||||||
|
sportsSeasonIds.length === 1
|
||||||
|
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
||||||
|
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds),
|
||||||
|
draftedParticipantIds.length > 0
|
||||||
|
? notInArray(schema.participants.id, draftedParticipantIds)
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
orderBy: desc(schema.participants.expectedValue),
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (availableParticipants.length === 0) {
|
||||||
|
console.error(`[Timer] No available participants for auto-pick in season ${seasonId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
participantId = availableParticipants[0].id;
|
||||||
|
console.log(`[Timer] Auto-picking highest EV: ${participantId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate round and pick in round
|
||||||
|
const draftSlots = await db.query.draftSlots.findMany({
|
||||||
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||||
|
orderBy: asc(schema.draftSlots.draftOrder),
|
||||||
|
});
|
||||||
|
const totalTeams = draftSlots.length;
|
||||||
|
const round = Math.ceil(pickNumber / totalTeams);
|
||||||
|
const pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
||||||
|
|
||||||
|
// Create the draft pick
|
||||||
|
const [createdPick] = await db.insert(schema.draftPicks).values({
|
||||||
|
seasonId,
|
||||||
|
teamId,
|
||||||
|
participantId,
|
||||||
|
pickNumber,
|
||||||
|
round,
|
||||||
|
pickInRound,
|
||||||
|
pickedByUserId: "", // Auto-pick has no user
|
||||||
|
pickedByType: "auto",
|
||||||
|
timeUsed: season.draftInitialTime || 120, // Used all time
|
||||||
|
createdAt: new Date(),
|
||||||
|
}).returning();
|
||||||
|
|
||||||
|
console.log(`[Timer] Auto-pick created: Pick ${pickNumber} - Participant ${participantId}`);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if draft is complete
|
||||||
|
const totalPicks = totalTeams * (season.draftRounds || 1);
|
||||||
|
const allPicks = await db.query.draftPicks.findMany({
|
||||||
|
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||||
|
});
|
||||||
|
const isDraftComplete = allPicks.length >= totalPicks;
|
||||||
|
const nextPickNumber = isDraftComplete ? pickNumber : pickNumber + 1;
|
||||||
|
|
||||||
|
// Add increment to the team that just picked
|
||||||
|
const pickingTeamTimer = await db.query.draftTimers.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.draftTimers.seasonId, seasonId),
|
||||||
|
eq(schema.draftTimers.teamId, teamId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pickingTeamTimer) {
|
||||||
|
const incrementTime = season.draftIncrementTime || 30;
|
||||||
|
const newTimeRemaining = pickingTeamTimer.timeRemaining + incrementTime;
|
||||||
|
await db
|
||||||
|
.update(schema.draftTimers)
|
||||||
|
.set({
|
||||||
|
timeRemaining: newTimeRemaining,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.draftTimers.id, pickingTeamTimer.id));
|
||||||
|
|
||||||
|
// Emit timer update to all clients
|
||||||
|
io.to(`draft-${seasonId}`).emit("timer-update", {
|
||||||
|
seasonId,
|
||||||
|
teamId,
|
||||||
|
timeRemaining: newTimeRemaining,
|
||||||
|
currentPickNumber: pickNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDraftComplete) {
|
||||||
|
// Draft complete
|
||||||
|
await db
|
||||||
|
.update(schema.seasons)
|
||||||
|
.set({ status: "active" })
|
||||||
|
.where(eq(schema.seasons.id, seasonId));
|
||||||
|
|
||||||
|
io.to(`draft-${seasonId}`).emit("draft-completed");
|
||||||
|
console.log(`[Timer] Draft completed for season ${seasonId}`);
|
||||||
|
} else {
|
||||||
|
// Move to next pick
|
||||||
|
await db
|
||||||
|
.update(schema.seasons)
|
||||||
|
.set({ currentPickNumber: nextPickNumber })
|
||||||
|
.where(eq(schema.seasons.id, seasonId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the complete pick with all relations (same as API routes do)
|
||||||
|
const completePick = await db
|
||||||
|
.select({
|
||||||
|
id: schema.draftPicks.id,
|
||||||
|
seasonId: schema.draftPicks.seasonId,
|
||||||
|
teamId: schema.draftPicks.teamId,
|
||||||
|
participantId: schema.draftPicks.participantId,
|
||||||
|
pickNumber: schema.draftPicks.pickNumber,
|
||||||
|
round: schema.draftPicks.round,
|
||||||
|
pickInRound: schema.draftPicks.pickInRound,
|
||||||
|
pickedByUserId: schema.draftPicks.pickedByUserId,
|
||||||
|
pickedByType: schema.draftPicks.pickedByType,
|
||||||
|
timeUsed: schema.draftPicks.timeUsed,
|
||||||
|
createdAt: schema.draftPicks.createdAt,
|
||||||
|
team: schema.teams,
|
||||||
|
participant: schema.participants,
|
||||||
|
sport: schema.sports,
|
||||||
|
})
|
||||||
|
.from(schema.draftPicks)
|
||||||
|
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||||
|
.innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id))
|
||||||
|
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||||
|
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||||
|
.where(eq(schema.draftPicks.id, createdPick.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (completePick.length === 0) {
|
||||||
|
console.error(`[Timer] Could not fetch complete pick data`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit pick-made event with the same structure as API routes
|
||||||
|
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||||
|
pick: {
|
||||||
|
...completePick[0],
|
||||||
|
participant: {
|
||||||
|
...completePick[0].participant,
|
||||||
|
sport: completePick[0].sport,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextPickNumber,
|
||||||
|
isDraftComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Timer] Auto-pick complete and broadcasted`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Timer] Error in triggerAutoPick:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue