From 714ab0f48437662c2d13edbea6b9cddf53d23f04 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 25 Oct 2025 10:14:36 -0700 Subject: [PATCH] feat: Refactor autopick logic to use unified executeAutoPick function and enhance eligibility checks --- app/models/draft-utils.ts | 369 ++++++++++++++++++++++++- app/routes/api/draft.force-autopick.ts | 175 ++---------- server/timer.ts | 253 +---------------- tsconfig.node.json | 2 + tsconfig.server.json | 4 +- 5 files changed, 396 insertions(+), 407 deletions(-) diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index fbbe9f3..aae3898 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -9,7 +9,7 @@ import { calculateDraftEligibility } from "~/lib/draft-eligibility"; /** * Auto-pick for a team when their timer runs out - * 1. Check queue - pick first eligible item if available + * 1. Check queue - pick first eligible item if available (cleans up ineligible items) * 2. If queue empty, pick highest EV participant not drafted from eligible sports * * Updated to respect Omni league draft eligibility rules @@ -39,11 +39,18 @@ export async function autoPickForTeam( allTeams ); + console.log( + `[AutoPick] Team ${teamId} eligible sports:`, + Array.from(eligibility.eligibleSportIds) + ); + // Check queue first - filter by eligible sports const queue = await getTeamQueue(teamId); if (queue.length > 0) { - // Get participant details for queue items to check sport eligibility + console.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`); + + // Get participant details for queue items to check eligibility const queueParticipantIds = queue.map((item) => item.participantId); const queueParticipants = await db.query.participants.findMany({ where: inArray(schema.participants.id, queueParticipantIds), @@ -56,23 +63,67 @@ export async function autoPickForTeam( }, }); - // Try queue items in order, checking both drafted status and eligibility + const ineligibleQueueItemIds: string[] = []; + + // Try queue items in order, checking both drafted status and sport eligibility for (const item of queue) { const participant = queueParticipants.find((p) => p.id === item.participantId); - if (!participant) continue; + if (!participant) { + console.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`); + ineligibleQueueItemIds.push(item.id); + continue; + } const sportId = participant.sportsSeason.sport.id; const isEligible = eligibility.eligibleSportIds.has(sportId); const isDrafted = await isParticipantDrafted(seasonId, item.participantId); - if (!isDrafted && isEligible) { - return item.participantId; + if (isDrafted) { + console.log( + `[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove` + ); + ineligibleQueueItemIds.push(item.id); + continue; } + + if (!isEligible) { + console.log( + `[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - not eligible for this team, will remove` + ); + ineligibleQueueItemIds.push(item.id); + continue; + } + + // Found a valid pick from queue + console.log( + `[AutoPick] Selecting from queue: ${participant.name} (${participant.sportsSeason.sport.name})` + ); + + // Clean up ineligible items from queue before returning + if (ineligibleQueueItemIds.length > 0) { + await db + .delete(schema.draftQueue) + .where(inArray(schema.draftQueue.id, ineligibleQueueItemIds)); + console.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`); + } + + return item.participantId; + } + + // All queue items were ineligible or drafted - clean them up + if (ineligibleQueueItemIds.length > 0) { + await db + .delete(schema.draftQueue) + .where(inArray(schema.draftQueue.id, ineligibleQueueItemIds)); + console.log( + `[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue (all items were invalid)` + ); } } // Queue is empty or all queued players drafted/ineligible // Pick highest EV available from eligible sports + console.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`); return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds); } @@ -224,9 +275,311 @@ export function getTeamForPick( ): string | null { const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder); const teamCount = sortedOrder.length; - + if (teamCount === 0) return null; - + const { teamIndex } = calculatePickInfo(pickNumber, teamCount); return sortedOrder[teamIndex]?.teamId || null; } + +/** + * Execute an autopick for a team - unified function for both commissioner-forced and timer-based autopicks + * + * Selection Logic: + * 1. Uses the team's draft queue first (prioritizes manager's preferences) + * 2. Validates each queued participant for: + * - Not already drafted + * - Eligible based on draft rules (sport eligibility, flex spots, etc.) + * 3. Automatically removes ineligible participants from queue + * 4. If queue is empty or all items are ineligible, selects highest EV participant from eligible sports + * + * This ensures autopicks ALWAYS respect draft eligibility rules and cannot make illegal selections. + * + * @param params.seasonId - The season ID + * @param params.teamId - The team making the pick + * @param params.pickNumber - The current pick number + * @param params.triggeredBy - Who/what triggered the autopick ("commissioner" or "timer") + * @param params.commissionerUserId - User ID of commissioner (required if triggeredBy is "commissioner") + * @param params.autodraftSettings - Autodraft settings (used for timer-based picks) + * @param params.db - Database instance (optional, will use database() if not provided) + * @returns Result object with success status and pick data + */ +export async function executeAutoPick(params: { + seasonId: string; + teamId: string; + pickNumber: number; + triggeredBy: "commissioner" | "timer"; + commissionerUserId?: string; + autodraftSettings?: any; + db?: ReturnType; +}): Promise<{ + success: boolean; + error?: string; + pick?: any; + participant?: any; + nextPickNumber?: number; + isDraftComplete?: boolean; +}> { + const { + seasonId, + teamId, + pickNumber, + triggeredBy, + commissionerUserId, + autodraftSettings, + db: providedDb, + } = params; + + const db = providedDb || database(); + + try { + // Race condition protection - check if pick already made + const existingPick = await db.query.draftPicks.findFirst({ + where: and( + eq(schema.draftPicks.seasonId, seasonId), + eq(schema.draftPicks.pickNumber, pickNumber) + ), + }); + + if (existingPick) { + console.log(`[AutoPick] Pick ${pickNumber} already made, skipping`); + return { + success: false, + error: "Pick already made", + }; + } + + // Get season details + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + }); + + if (!season) { + return { + success: false, + error: "Season not found", + }; + } + + // Get draft slots to calculate round/pickInRound and get all team IDs + const draftSlots = await db.query.draftSlots.findMany({ + where: eq(schema.draftSlots.seasonId, seasonId), + orderBy: schema.draftSlots.draftOrder, + with: { + team: true, + }, + }); + + const totalTeams = draftSlots.length; + if (totalTeams === 0) { + return { + success: false, + error: "No draft slots found", + }; + } + + const allTeamIds = draftSlots.map((slot) => slot.teamId); + + // Use autoPickForTeam to select participant (respects eligibility and queue) + const participantId = await autoPickForTeam( + seasonId, + teamId, + season.draftRounds, + allTeamIds + ); + + if (!participantId) { + return { + success: false, + error: "No eligible participants available to pick", + }; + } + + // Get participant details + const participantToPick = await db.query.participants.findFirst({ + where: eq(schema.participants.id, participantId), + with: { + sportsSeason: { + with: { + sport: true, + }, + }, + }, + }); + + if (!participantToPick) { + return { + success: false, + error: "Participant not found", + }; + } + + // Calculate round and pickInRound with snake draft logic + const currentRound = Math.ceil(pickNumber / totalTeams); + const isEvenRound = currentRound % 2 === 0; + let pickInRound = ((pickNumber - 1) % totalTeams) + 1; + + // Apply snake draft reversal for even rounds + if (isEvenRound) { + pickInRound = totalTeams - pickInRound + 1; + } + + // Determine pickedByUserId based on trigger + const pickedByUserId = triggeredBy === "commissioner" + ? (commissionerUserId || "") + : ""; + + // Create the draft pick + const [draftPick] = await db + .insert(schema.draftPicks) + .values({ + seasonId, + teamId, + participantId: participantToPick.id, + pickNumber, + round: currentRound, + pickInRound, + pickedByUserId, + pickedByType: "auto", + timeUsed: triggeredBy === "timer" ? (season.draftInitialTime || 120) : undefined, + }) + .returning(); + + console.log( + `[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}` + ); + + // Update season's current pick number and check if draft complete + 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; + if (io) { + io.to(`draft-${seasonId}`).emit("timer-update", { + seasonId, + teamId, + timeRemaining: newTimeRemaining, + currentPickNumber: pickNumber, + }); + } + } catch (error) { + console.error("[AutoPick] 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) + ) + ); + + // Handle autodraft settings for timer-based picks with "next_pick" mode + if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") { + console.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`); + await db + .update(schema.autodraftSettings) + .set({ + isEnabled: false, + updatedAt: new Date(), + }) + .where(eq(schema.autodraftSettings.id, autodraftSettings.id)); + + // Emit autodraft-updated event + try { + const io = (global as any).__socketIO; + if (io) { + io.to(`draft-${seasonId}`).emit("autodraft-updated", { + teamId, + isEnabled: false, + mode: autodraftSettings.mode, + }); + } + } catch (error) { + console.error("[AutoPick] Socket.IO autodraft-updated error:", error); + } + } + + // Emit socket events + try { + const io = (global as any).__socketIO; + if (io) { + const team = draftSlots.find((slot) => slot.team.id === teamId)?.team; + + // Emit participant-removed-from-queues event + io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { + participantId: participantToPick.id, + }); + + // Emit pick-made event + io.to(`draft-${seasonId}`).emit("pick-made", { + pick: { + ...draftPick, + team, + participant: { + ...participantToPick, + sport: participantToPick.sportsSeason.sport, + }, + sport: participantToPick.sportsSeason.sport, + }, + nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber, + isDraftComplete, + }); + + // Emit draft-completed event if applicable + if (isDraftComplete) { + io.to(`draft-${seasonId}`).emit("draft-completed"); + } + } + } catch (error) { + console.error("[AutoPick] Socket.IO events error:", error); + } + + return { + success: true, + pick: draftPick, + participant: participantToPick, + nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber, + isDraftComplete, + }; + } catch (error) { + console.error("[AutoPick] Error in executeAutoPick:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} diff --git a/app/routes/api/draft.force-autopick.ts b/app/routes/api/draft.force-autopick.ts index 20d8513..0df9e8a 100644 --- a/app/routes/api/draft.force-autopick.ts +++ b/app/routes/api/draft.force-autopick.ts @@ -1,8 +1,8 @@ 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 { autoPickForTeam } from "~/models/draft-utils"; +import { eq } from "drizzle-orm"; +import { executeAutoPick } from "~/models/draft-utils"; export async function action(args: any) { const { request } = args; @@ -24,7 +24,7 @@ export async function action(args: any) { const db = database(); - // Get season details + // Get season details to verify commissioner permissions const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), with: { @@ -41,166 +41,25 @@ export async function action(args: any) { return Response.json({ error: "Only commissioner can force autopick" }, { status: 403 }); } - // Get draft slots to pass team IDs - const draftSlots = await db.query.draftSlots.findMany({ - where: eq(schema.draftSlots.seasonId, seasonId), - orderBy: schema.draftSlots.draftOrder, - with: { - team: true, - }, - }); - - const allTeamIds = draftSlots.map((slot) => slot.teamId); - - // Use autoPickForTeam which now respects eligibility - const participantId = await autoPickForTeam( + // Execute the autopick using the unified function + const result = await executeAutoPick({ seasonId, teamId, - season.draftRounds, - allTeamIds - ); - - if (!participantId) { - return Response.json({ error: "No eligible participants available to pick" }, { status: 400 }); - } - - // Get participant details - const participantToPick = await db.query.participants.findFirst({ - where: eq(schema.participants.id, participantId), - with: { - sportsSeason: { - with: { - sport: true, - }, - }, - }, + pickNumber, + triggeredBy: "commissioner", + commissionerUserId: userId, + db, }); - if (!participantToPick) { - return Response.json({ error: "Participant not found" }, { status: 404 }); + if (!result.success) { + return Response.json({ error: result.error }, { status: 400 }); } - // Calculate round and pickInRound - 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; - } - - // Create the draft pick - const [draftPick] = await db - .insert(schema.draftPicks) - .values({ - seasonId, - teamId, - participantId: participantToPick.id, - pickNumber, - round: currentRound, - pickInRound, - pickedByUserId: userId, - pickedByType: "auto", - }) - .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, participantToPick.id) - ) - ); - - // Notify all clients that this participant was removed from queues - try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { - participantId: participantToPick.id, - }); - } catch (error) { - console.error("Socket.IO participant-removed-from-queues error:", error); - } - - // 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: { - ...participantToPick, - sport: participantToPick.sportsSeason.sport, - }, - sport: participantToPick.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, - participant: participantToPick, - nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber, - isDraftComplete, + return Response.json({ + success: true, + pick: result.pick, + participant: result.participant, + nextPickNumber: result.nextPickNumber, + isDraftComplete: result.isDraftComplete, }); } diff --git a/server/timer.ts b/server/timer.ts index b6735ed..95e2739 100644 --- a/server/timer.ts +++ b/server/timer.ts @@ -1,8 +1,9 @@ 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 { eq, and, asc } from "drizzle-orm"; import { getSocketIO } from "./socket"; +import { executeAutoPick } from "~/models/draft-utils"; // Create a dedicated database connection for the timer const connectionString = process.env.DATABASE_URL; @@ -186,6 +187,7 @@ async function updateDraftTimers(): Promise { /** * Trigger an automatic pick when timer expires + * Uses the unified executeAutoPick function */ async function triggerAutoPick( seasonId: string, @@ -194,254 +196,25 @@ async function triggerAutoPick( autodraftSettings: any | null ): Promise { try { - const io = getSocketIO(); + console.log( + `[Timer] Triggering auto-pick for team ${teamId} in season ${seasonId}, pick ${pickNumber}` + ); - // 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({ + // Execute the autopick using the unified function + const result = await executeAutoPick({ 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; - - // If autodraft was enabled with "next_pick" mode, disable it now - if (autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") { - console.log(`[Timer] Disabling autodraft for team ${teamId} after next_pick`); - await db - .update(schema.autodraftSettings) - .set({ - isEnabled: false, - updatedAt: new Date(), - }) - .where(eq(schema.autodraftSettings.id, autodraftSettings.id)); - - // Emit socket event to notify clients - io.to(`draft-${seasonId}`).emit("autodraft-updated", { - teamId, - isEnabled: false, - mode: autodraftSettings.mode, - }); - } - - // 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) - ), + triggeredBy: "timer", + autodraftSettings, + db, }); - 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`); + if (!result.success) { + console.error(`[Timer] Auto-pick failed: ${result.error}`); 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); diff --git a/tsconfig.node.json b/tsconfig.node.json index 15611b1..a5539d9 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,6 +4,8 @@ "server/**/*.ts", "database/**/*.ts", "app/contexts/**/*.ts", + "app/models/**/*.ts", + "app/lib/**/*.ts", "vite.config.ts" ], "exclude": [ diff --git a/tsconfig.server.json b/tsconfig.server.json index 9affce8..0b746f2 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -28,7 +28,9 @@ "server.ts", "server/**/*.ts", "database/**/*.ts", - "app/database/**/*.ts" + "app/database/**/*.ts", + "app/models/**/*.ts", + "app/lib/**/*.ts" ], "exclude": [ "node_modules",