brackt/app/models/draft-slot.ts
Chris Parsons 0ba2b391f8
Fix draft order initialization for teams added mid-season (#449)
* Fix draft order showing only new team when league size increased before order was set

When a commissioner increased the team count on a league that had teams
but no draft order set yet, the server created draft slots only for the
newly-added teams. This left the DB with N+K teams but only K slots,
causing the drag-and-drop list to display only the K new teams.

Two fixes:
1. Server: only append new draft slots when an order was already set
   (existingSlots.length > 0). If no order exists yet, skip slot creation
   so the page correctly treats the order as unset for all teams.
2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the
   slotted ones, so a partially-corrupt DB state still shows all teams.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Address code review feedback on draft order bug fix

- Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable
  and testable; remove the local copy from the settings component
- Add unit tests for buildDraftOrderTeams covering the empty, full, and
  partial-slot cases
- Tighten the draft slot guard from existingSlots.length > 0 to
  existingSlots.length === currentTeamCount so partial legacy states are
  treated the same as "order not set"
- Condense the 3-line server comment to a single line per project style
- Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what
  the function actually counts, and update its one call site
- Add tests for the server-side slot-creation guard logic

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Fix lint: move shouldAppendDraftSlots to outer scope

oxlint (consistent-function-scoping) requires functions that don't close
over any variables to be defined at the outer scope rather than inside a
describe block.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00

160 lines
3.9 KiB
TypeScript

import { eq, count } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import type { RawFlagConfig } from "~/lib/flag-types";
export type DraftSlot = typeof schema.draftSlots.$inferSelect;
export type NewDraftSlot = typeof schema.draftSlots.$inferInsert;
export interface DraftSlotWithTeam extends DraftSlot {
team: {
id: string;
name: string;
ownerId: string | null;
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
};
}
/**
* Create a single draft slot
*/
export async function createDraftSlot(data: NewDraftSlot): Promise<DraftSlot> {
const db = database();
const [draftSlot] = await db
.insert(schema.draftSlots)
.values(data)
.returning();
return draftSlot;
}
/**
* Create multiple draft slots at once
*/
export async function createManyDraftSlots(
slots: NewDraftSlot[]
): Promise<DraftSlot[]> {
const db = database();
return await db.insert(schema.draftSlots).values(slots).returning();
}
/**
* Find all draft slots for a season, ordered by draft order
*/
export async function findDraftSlotsBySeasonId(
seasonId: string
): Promise<DraftSlotWithTeam[]> {
const db = database();
return await db.query.draftSlots.findMany({
where: eq(schema.draftSlots.seasonId, seasonId),
orderBy: (draftSlots, { asc }) => [asc(draftSlots.draftOrder)],
with: {
team: {
columns: {
id: true,
name: true,
ownerId: true,
logoUrl: true,
flagConfig: true,
avatarType: true,
},
},
},
}) as DraftSlotWithTeam[];
}
/**
* Find a draft slot by team ID
*/
export async function findDraftSlotByTeamId(
teamId: string
): Promise<DraftSlot | undefined> {
const db = database();
return await db.query.draftSlots.findFirst({
where: eq(schema.draftSlots.teamId, teamId),
});
}
/**
* Update a draft slot's order
*/
export async function updateDraftSlotOrder(
id: string,
draftOrder: number
): Promise<DraftSlot> {
const db = database();
const [draftSlot] = await db
.update(schema.draftSlots)
.set({ draftOrder, updatedAt: new Date() })
.where(eq(schema.draftSlots.id, id))
.returning();
return draftSlot;
}
/**
* Delete all draft slots for a season
*/
export async function deleteDraftSlotsBySeasonId(
seasonId: string
): Promise<void> {
const db = database();
await db
.delete(schema.draftSlots)
.where(eq(schema.draftSlots.seasonId, seasonId));
}
/**
* Delete a specific draft slot
*/
export async function deleteDraftSlot(id: string): Promise<void> {
const db = database();
await db.delete(schema.draftSlots).where(eq(schema.draftSlots.id, id));
}
/**
* Set the draft order for all teams in a season
* This will delete existing slots and create new ones
*/
export async function setDraftOrder(
seasonId: string,
teamIds: string[]
): Promise<DraftSlot[]> {
// Delete existing draft slots for this season
await deleteDraftSlotsBySeasonId(seasonId);
// Create new draft slots with the specified order
const slots = teamIds.map((teamId, index) => ({
seasonId,
teamId,
draftOrder: index + 1,
}));
return await createManyDraftSlots(slots);
}
export async function getNumDraftSlotsBySeasonId(seasonId: string): Promise<number> {
const db = database();
const [result] = await db
.select({ count: count() })
.from(schema.draftSlots)
.where(eq(schema.draftSlots.seasonId, seasonId));
return result?.count ?? 0;
}
/**
* Randomize the draft order for a season
*/
export async function randomizeDraftOrder(
seasonId: string,
teamIds: string[]
): Promise<DraftSlot[]> {
// Shuffle the team IDs using Fisher-Yates algorithm
const shuffled = [...teamIds];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return await setDraftOrder(seasonId, shuffled);
}