feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and remove any queued participants whose sport is no longer eligible (e.g. a team queued a snooker player but just filled their last flex slot). Previously this was only caught lazily when autodraft fired, which could pause the draft or pick an unwanted player. - Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows for a season in one query (Map<teamId, QueueItem[]>) instead of N+1 per-team queries - Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all for the four required data fetches, collects ineligible items in a single loop pass, warns on orphaned participant references - Call from both pick paths: executeAutoPick and draft.make-pick.ts - Emit queue-eligibility-pruned socket event per affected team so the client updates the queue UI in real time - Add 5 tests covering: single ineligible removal, all eligible (no-op), empty queues (no delete called, getTeamQueue never called), mixed queue (only ineligible item removed), and unknown participant (warn) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09918f4b38
commit
16aa450d63
5 changed files with 441 additions and 4 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { autoPickForTeam } from "../draft-utils";
|
||||
import { autoPickForTeam, pruneIneligibleQueueItems } from "../draft-utils";
|
||||
|
||||
// Mock all external model dependencies so we can control their return values
|
||||
vi.mock("~/models/draft-pick", () => ({
|
||||
|
|
@ -18,6 +18,7 @@ vi.mock("~/models/season-sport", () => ({
|
|||
|
||||
vi.mock("~/models/draft-queue", () => ({
|
||||
getTeamQueue: vi.fn(),
|
||||
getAllQueuesForSeason: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("~/lib/draft-eligibility", () => ({
|
||||
|
|
@ -35,7 +36,7 @@ import {
|
|||
} from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||
import { getTeamQueue } from "~/models/draft-queue";
|
||||
import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
|
||||
const SEASON_ID = "season-1";
|
||||
|
|
@ -217,6 +218,108 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("ineligible sport — only queued player's sport is full", () => {
|
||||
it("removes the ineligible queue item and returns null when queueOnly=true", async () => {
|
||||
// Scenario: team has a snooker player queued but snooker is already full (not in eligibleSportIds)
|
||||
const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
|
||||
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
|
||||
|
||||
// Snooker is NOT eligible — only "sport-other" is
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue({
|
||||
eligibleSportIds: new Set(["sport-other"]),
|
||||
} as ReturnType<typeof calculateDraftEligibility>);
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-snooker",
|
||||
name: "Ronnie O'Sullivan",
|
||||
sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
delete: mockDelete,
|
||||
where: mockWhere,
|
||||
});
|
||||
|
||||
vi.mocked(isParticipantDrafted).mockResolvedValue(false); // not drafted — ineligible due to sport
|
||||
|
||||
const result = await autoPickForTeam(
|
||||
SEASON_ID,
|
||||
TEAM_ID,
|
||||
DRAFT_ROUNDS,
|
||||
ALL_TEAM_IDS,
|
||||
mockDb as any,
|
||||
true // queueOnly — no EV fallback
|
||||
);
|
||||
|
||||
// Queue item should be removed
|
||||
expect(mockDelete).toHaveBeenCalled();
|
||||
// No valid pick available
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to EV pick from eligible sports when queueOnly=false", async () => {
|
||||
// Scenario: same ineligible snooker player, but queueOnly is off so system picks best available
|
||||
const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
|
||||
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
|
||||
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue({
|
||||
eligibleSportIds: new Set(["sport-other"]),
|
||||
} as ReturnType<typeof calculateDraftEligibility>);
|
||||
|
||||
// where() call order:
|
||||
// 1. delete().where() — remove ineligible snooker item from queue
|
||||
// 2. select().from().where() — getTopAvailableParticipant: drafted picks → []
|
||||
// 3. select().from().innerJoin().innerJoin().where() — season sports → [{...}]
|
||||
// 4. select().from(participants).where().orderBy() — participant query (chain)
|
||||
const mockDb = {
|
||||
query: {
|
||||
participants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-snooker",
|
||||
name: "Ronnie O'Sullivan",
|
||||
sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
where: vi.fn()
|
||||
.mockResolvedValueOnce(undefined) // call 1: delete ineligible queue items
|
||||
.mockResolvedValueOnce([]) // call 2: drafted picks
|
||||
.mockResolvedValueOnce([{ sportsSeasonId: "ss-other", sportId: "sport-other" }]) // call 3: season sports
|
||||
.mockReturnThis(), // call 4: participant query → chain to orderBy
|
||||
orderBy: vi.fn().mockResolvedValue([
|
||||
{ id: "p-ev", name: "EV Player", expectedValue: "90.00" },
|
||||
]),
|
||||
delete: vi.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
vi.mocked(isParticipantDrafted).mockResolvedValue(false);
|
||||
|
||||
const result = await autoPickForTeam(
|
||||
SEASON_ID,
|
||||
TEAM_ID,
|
||||
DRAFT_ROUNDS,
|
||||
ALL_TEAM_IDS,
|
||||
mockDb as any,
|
||||
false // queueOnly off — should fall back to best available
|
||||
);
|
||||
|
||||
// Should pick the best available from eligible sports, not the snooker player
|
||||
expect(result).toBe("p-ev");
|
||||
});
|
||||
});
|
||||
|
||||
describe("partial queue — first item drafted, second available", () => {
|
||||
it("skips drafted item and returns the next valid participant", async () => {
|
||||
const queue = [
|
||||
|
|
@ -302,3 +405,187 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pruneIneligibleQueueItems", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
// Helper to build the Map<teamId, queue[]> returned by getAllQueuesForSeason
|
||||
function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) {
|
||||
return new Map(entries.map(([teamId, items]) => [teamId, items as any]));
|
||||
}
|
||||
|
||||
it("removes queued snooker player when snooker is no longer eligible for a team", async () => {
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
||||
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
||||
] as any);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
|
||||
// team-1: snooker ineligible; team-2: snooker still eligible
|
||||
vi.mocked(calculateDraftEligibility)
|
||||
.mockReturnValueOnce({ eligibleSportIds: new Set([]) } as any)
|
||||
.mockReturnValueOnce({ eligibleSportIds: new Set(["sport-snooker"]) } as any);
|
||||
|
||||
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
||||
makeSeasonQueues([
|
||||
[TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
|
||||
["team-2", []],
|
||||
])
|
||||
);
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });
|
||||
|
||||
const result = await pruneIneligibleQueueItems({
|
||||
seasonId: SEASON_ID,
|
||||
draftRounds: DRAFT_ROUNDS,
|
||||
allTeamIds: ALL_TEAM_IDS,
|
||||
db: mockDb as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].teamId).toBe(TEAM_ID);
|
||||
expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
|
||||
expect(mockDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns empty array when all teams still have their sports eligible", async () => {
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
||||
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
||||
] as any);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
||||
{ eligibleSportIds: new Set(["sport-snooker"]) } as any
|
||||
);
|
||||
|
||||
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
||||
makeSeasonQueues([
|
||||
[TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
|
||||
["team-2", [{ id: "q-2", participantId: "p-snooker", queuePosition: 1 }]],
|
||||
])
|
||||
);
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockDb = makeMockDb({ delete: mockDelete });
|
||||
|
||||
const result = await pruneIneligibleQueueItems({
|
||||
seasonId: SEASON_ID,
|
||||
draftRounds: DRAFT_ROUNDS,
|
||||
allTeamIds: ALL_TEAM_IDS,
|
||||
db: mockDb as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(mockDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips teams with empty queues and does not call delete", async () => {
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
||||
{ eligibleSportIds: new Set([]) } as any
|
||||
);
|
||||
|
||||
// Both teams return no queue items from the single bulk fetch
|
||||
vi.mocked(getAllQueuesForSeason).mockResolvedValue(new Map());
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockDb = makeMockDb({ delete: mockDelete });
|
||||
|
||||
const result = await pruneIneligibleQueueItems({
|
||||
seasonId: SEASON_ID,
|
||||
draftRounds: DRAFT_ROUNDS,
|
||||
allTeamIds: ALL_TEAM_IDS,
|
||||
db: mockDb as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(mockDelete).not.toHaveBeenCalled();
|
||||
// The bulk fetch replaces per-team queries — getTeamQueue should never be called
|
||||
expect(vi.mocked(getTeamQueue)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prunes only the ineligible items when a team has a mixed queue", async () => {
|
||||
// Team has two queued players: snooker (ineligible) and NFL (still eligible)
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
||||
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
||||
{ id: "p-nfl", name: "Patrick Mahomes", sport: { id: "sport-nfl", name: "NFL" } },
|
||||
] as any);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
||||
{ eligibleSportIds: new Set(["sport-nfl"]) } as any // snooker full, NFL still ok
|
||||
);
|
||||
|
||||
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
||||
makeSeasonQueues([
|
||||
[TEAM_ID, [
|
||||
{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 },
|
||||
{ id: "q-nfl", participantId: "p-nfl", queuePosition: 2 },
|
||||
]],
|
||||
])
|
||||
);
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });
|
||||
|
||||
const result = await pruneIneligibleQueueItems({
|
||||
seasonId: SEASON_ID,
|
||||
draftRounds: DRAFT_ROUNDS,
|
||||
allTeamIds: [TEAM_ID],
|
||||
db: mockDb as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
|
||||
// The NFL player must NOT be removed
|
||||
expect(result[0].removedParticipantIds).not.toContain("p-nfl");
|
||||
expect(mockDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips a queue item whose participant is not found in season sports and logs a warning", async () => {
|
||||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
// allParticipants does NOT include p-mystery
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
||||
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
||||
{ eligibleSportIds: new Set([]) } as any
|
||||
);
|
||||
|
||||
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
||||
makeSeasonQueues([
|
||||
[TEAM_ID, [{ id: "q-mystery", participantId: "p-mystery", queuePosition: 1 }]],
|
||||
])
|
||||
);
|
||||
|
||||
const mockDelete = vi.fn().mockReturnThis();
|
||||
const mockDb = makeMockDb({ delete: mockDelete });
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
const result = await pruneIneligibleQueueItems({
|
||||
seasonId: SEASON_ID,
|
||||
draftRounds: DRAFT_ROUNDS,
|
||||
allTeamIds: [TEAM_ID],
|
||||
db: mockDb as any,
|
||||
});
|
||||
|
||||
// Item is skipped — not deleted, not returned
|
||||
expect(result).toHaveLength(0);
|
||||
expect(mockDelete).not.toHaveBeenCalled();
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("p-mystery"));
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,6 +86,29 @@ export async function removeParticipantFromQueue(teamId: string, participantId:
|
|||
);
|
||||
}
|
||||
|
||||
export async function getAllQueuesForSeason(
|
||||
seasonId: string,
|
||||
providedDb?: ReturnType<typeof database>
|
||||
): Promise<Map<string, typeof schema.draftQueue.$inferSelect[]>> {
|
||||
const db = providedDb || database();
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schema.draftQueue)
|
||||
.where(eq(schema.draftQueue.seasonId, seasonId))
|
||||
.orderBy(asc(schema.draftQueue.queuePosition));
|
||||
|
||||
const byTeam = new Map<string, typeof schema.draftQueue.$inferSelect[]>();
|
||||
for (const row of rows) {
|
||||
const existing = byTeam.get(row.teamId);
|
||||
if (existing) {
|
||||
existing.push(row);
|
||||
} else {
|
||||
byTeam.set(row.teamId, [row]);
|
||||
}
|
||||
}
|
||||
return byTeam;
|
||||
}
|
||||
|
||||
export async function clearAllQueuesForSeason(seasonId: string) {
|
||||
const db = database();
|
||||
await db
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { database } from "~/database/context";
|
|||
import * as schema from "~/database/schema";
|
||||
import { eq, and, notInArray, desc, inArray, sql, asc } from "drizzle-orm";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { getTeamQueue } from "./draft-queue";
|
||||
import { getTeamQueue, getAllQueuesForSeason } from "./draft-queue";
|
||||
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "./participant";
|
||||
import { getSeasonSportsSimple } from "./season-sport";
|
||||
|
|
@ -354,6 +354,85 @@ export function getTeamForPick(
|
|||
return sortedOrder[teamIndex]?.teamId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* After a pick is committed, recalculate draft eligibility for every team and remove
|
||||
* any queued participants whose sport is no longer eligible for that team.
|
||||
*
|
||||
* This handles cases like: a team has a snooker player queued but has now filled their
|
||||
* last flex slot that snooker could have used — the pick should be proactively removed
|
||||
* rather than silently failing or pausing the draft later.
|
||||
*
|
||||
* Returns the per-team removals so the caller can emit socket events.
|
||||
*/
|
||||
export async function pruneIneligibleQueueItems(params: {
|
||||
seasonId: string;
|
||||
draftRounds: number;
|
||||
allTeamIds: string[];
|
||||
db: ReturnType<typeof database>;
|
||||
}): Promise<{ teamId: string; removedParticipantIds: string[] }[]> {
|
||||
const { seasonId, draftRounds, allTeamIds, db } = params;
|
||||
|
||||
const [allPicks, allParticipants, seasonSports, allQueues] = await Promise.all([
|
||||
getDraftPicksWithSports(seasonId, db),
|
||||
getParticipantsForSeasonWithSports(seasonId, db),
|
||||
getSeasonSportsSimple(seasonId, db),
|
||||
getAllQueuesForSeason(seasonId, db),
|
||||
]);
|
||||
|
||||
// Build a fast lookup: participantId → sportId
|
||||
const participantSportMap = new Map<string, string>();
|
||||
for (const p of allParticipants) {
|
||||
participantSportMap.set(p.id, p.sport.id);
|
||||
}
|
||||
|
||||
const allTeams = allTeamIds.map((id) => ({ id }));
|
||||
const results: { teamId: string; removedParticipantIds: string[] }[] = [];
|
||||
|
||||
for (const teamId of allTeamIds) {
|
||||
const queue = allQueues.get(teamId) ?? [];
|
||||
if (queue.length === 0) continue;
|
||||
|
||||
const teamPicks = allPicks.filter((p) => p.teamId === teamId);
|
||||
const eligibility = calculateDraftEligibility(
|
||||
teamId,
|
||||
teamPicks,
|
||||
allPicks,
|
||||
allParticipants,
|
||||
seasonSports,
|
||||
draftRounds,
|
||||
allTeams
|
||||
);
|
||||
|
||||
const ineligible: { id: string; participantId: string }[] = [];
|
||||
for (const item of queue) {
|
||||
const sportId = participantSportMap.get(item.participantId);
|
||||
if (sportId === undefined) {
|
||||
console.warn(
|
||||
`[QueuePrune] Team ${teamId}: queue item ${item.id} references participant ${item.participantId} not found in season sports — skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!eligibility.eligibleSportIds.has(sportId)) {
|
||||
ineligible.push({ id: item.id, participantId: item.participantId });
|
||||
}
|
||||
}
|
||||
|
||||
if (ineligible.length > 0) {
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(inArray(schema.draftQueue.id, ineligible.map((i) => i.id)));
|
||||
|
||||
const removedParticipantIds = ineligible.map((i) => i.participantId);
|
||||
results.push({ teamId, removedParticipantIds });
|
||||
console.log(
|
||||
`[QueuePrune] Team ${teamId}: removed ${ineligible.length} ineligible items (sport no longer eligible)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an autopick for a team - unified function for both commissioner-forced and timer-based autopicks
|
||||
*
|
||||
|
|
@ -618,6 +697,26 @@ export async function executeAutoPick(params: {
|
|||
)
|
||||
);
|
||||
|
||||
// Proactively prune queue items that are now ineligible due to this pick
|
||||
// (e.g. a team queued a snooker player but just filled their last flex slot)
|
||||
try {
|
||||
const prunedQueues = await pruneIneligibleQueueItems({
|
||||
seasonId,
|
||||
draftRounds: season.draftRounds,
|
||||
allTeamIds,
|
||||
db,
|
||||
});
|
||||
const io = getSocketIO();
|
||||
for (const { teamId: prunedTeamId, removedParticipantIds } of prunedQueues) {
|
||||
io.to(`draft-${seasonId}`).emit("queue-eligibility-pruned", {
|
||||
teamId: prunedTeamId,
|
||||
removedParticipantIds,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoPick] Error pruning ineligible queue items:", error);
|
||||
}
|
||||
|
||||
// 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`);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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 { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils";
|
||||
import { calculatePickInfo, checkAndTriggerNextAutodraft, pruneIneligibleQueueItems } from "~/models/draft-utils";
|
||||
import { getSocketIO } from "../../../server/socket";
|
||||
|
||||
import type { ActionFunctionArgs } from "react-router";
|
||||
|
|
@ -173,6 +173,26 @@ export async function action(args: ActionFunctionArgs) {
|
|||
console.error("Socket.IO participant-removed-from-queues error:", error);
|
||||
}
|
||||
|
||||
// Proactively prune queue items that are now ineligible due to this pick
|
||||
// (e.g. a team queued a snooker player but just filled their last flex slot)
|
||||
try {
|
||||
const allTeamIds = draftSlots.map((slot) => slot.teamId);
|
||||
const prunedQueues = await pruneIneligibleQueueItems({
|
||||
seasonId,
|
||||
draftRounds: season.draftRounds,
|
||||
allTeamIds,
|
||||
db,
|
||||
});
|
||||
for (const { teamId: prunedTeamId, removedParticipantIds } of prunedQueues) {
|
||||
getSocketIO().to(`draft-${seasonId}`).emit("queue-eligibility-pruned", {
|
||||
teamId: prunedTeamId,
|
||||
removedParticipantIds,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Queue pruning error after pick:", error);
|
||||
}
|
||||
|
||||
// Calculate next pick info (before updating season)
|
||||
const nextPickNumber = currentPickNumber + 1;
|
||||
const totalPicks = totalTeams * season.draftRounds;
|
||||
|
|
|
|||
|
|
@ -787,6 +787,12 @@ export default function DraftRoom() {
|
|||
);
|
||||
};
|
||||
|
||||
const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
|
||||
if (data.teamId !== userTeam?.id) return;
|
||||
const removed = new Set(data.removedParticipantIds);
|
||||
setQueue((prev: any) => prev.filter((item: any) => !removed.has(item.participantId)));
|
||||
};
|
||||
|
||||
const handlePickReplaced = (data: any) => {
|
||||
setPicks((prev: any) =>
|
||||
prev.map((p: any) => (p.pickNumber === data.pickNumber ? data.pick : p))
|
||||
|
|
@ -839,6 +845,7 @@ export default function DraftRoom() {
|
|||
on("team-disconnected", handleTeamDisconnected);
|
||||
on("connected-teams-list", handleConnectedTeamsList);
|
||||
on("participant-removed-from-queues", handleParticipantRemovedFromQueues);
|
||||
on("queue-eligibility-pruned", handleQueueEligibilityPruned);
|
||||
on("pick-replaced", handlePickReplaced);
|
||||
on("draft-rolled-back", handleDraftRolledBack);
|
||||
on("draft-state-sync", handleDraftStateSync);
|
||||
|
|
@ -854,6 +861,7 @@ export default function DraftRoom() {
|
|||
off("team-disconnected", handleTeamDisconnected);
|
||||
off("connected-teams-list", handleConnectedTeamsList);
|
||||
off("participant-removed-from-queues", handleParticipantRemovedFromQueues);
|
||||
off("queue-eligibility-pruned", handleQueueEligibilityPruned);
|
||||
off("pick-replaced", handlePickReplaced);
|
||||
off("draft-rolled-back", handleDraftRolledBack);
|
||||
off("draft-state-sync", handleDraftStateSync);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue