brackt/app/models/season.ts
Chris Parsons 2b680d7272
Replace isDraftable boolean with draftOn/draftOff date range (#263)
Fixes #262

* Replace isDraftable boolean with draftOn/draftOff date fields on sport seasons

Admins can now schedule when a sport season becomes available for drafting
by setting explicit open and close dates instead of a manual toggle.

https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc

* Address code review feedback on draft window implementation

- sports-data-sync: use unambiguous past placeholder dates for synced seasons,
  with a comment explaining the intent
- sports-season model: remove redundant top-level lte/gte imports (callback
  form already supplies them)
- _journal.json: add missing trailing newline
- sports-season test: mock ~/database/schema instead of stubbing all drizzle
  builder functions, matching the established pattern in the codebase
- admin list: compute today once in component body instead of per-row

https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc

* Fix: restore lte/gte imports removed in review cleanup

The relational query where-callback in this version of Drizzle does not
expose lte/gte as helper args, so they must be imported from drizzle-orm.
Removing them broke CI TypeScript.

https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 22:09:52 -04:00

210 lines
5.2 KiB
TypeScript

import { eq, and, sql } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
export type Season = typeof schema.seasons.$inferSelect;
export type NewSeason = Omit<typeof schema.seasons.$inferInsert, 'inviteCode'> & {
inviteCode?: string;
};
export type SeasonStatus = "pre_draft" | "draft" | "active" | "completed";
export interface SeasonWithSportsSeasons extends Season {
seasonSports: Array<{
id: string;
sportsSeason: {
id: string;
name: string;
year: number;
status: string;
scoringType: string;
scoringPattern: string | null;
draftOn: string; // 'YYYY-MM-DD'
draftOff: string; // 'YYYY-MM-DD'
sport: {
id: string;
name: string;
type: string;
slug: string;
};
};
}>;
}
export async function createSeason(data: NewSeason): Promise<Season> {
const db = database();
// Generate invite code if not provided
const seasonData = {
...data,
inviteCode: data.inviteCode || generateInviteCode(),
};
const [season] = await db
.insert(schema.seasons)
.values(seasonData)
.returning();
return season;
}
export async function findSeasonById(id: string): Promise<Season | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.id, id),
});
}
export async function findSeasonsByLeagueId(leagueId: string): Promise<Season[]> {
const db = database();
return await db.query.seasons.findMany({
where: eq(schema.seasons.leagueId, leagueId),
orderBy: (seasons, { desc }) => [desc(seasons.year)],
});
}
export async function findCurrentSeason(
leagueId: string
): Promise<Season | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.leagueId, leagueId),
orderBy: (seasons, { desc }) => [desc(seasons.year)],
});
}
export async function findCurrentSeasonWithSports(
leagueId: string
): Promise<SeasonWithSportsSeasons | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.leagueId, leagueId),
orderBy: (seasons, { desc }) => [desc(seasons.year)],
with: {
seasonSports: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
}) as SeasonWithSportsSeasons | undefined;
}
export async function findSeasonByLeagueAndYear(
leagueId: string,
year: number
): Promise<Season | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: and(
eq(schema.seasons.leagueId, leagueId),
eq(schema.seasons.year, year)
),
});
}
export async function updateSeason(
id: string,
data: Partial<NewSeason>
): Promise<Season> {
const db = database();
const [season] = await db
.update(schema.seasons)
.set({ ...data, updatedAt: new Date() })
.where(eq(schema.seasons.id, id))
.returning();
return season;
}
export async function updateSeasonStatus(
id: string,
status: SeasonStatus
): Promise<Season> {
return await updateSeason(id, { status });
}
export async function deleteSeason(id: string): Promise<void> {
const db = database();
await db.delete(schema.seasons).where(eq(schema.seasons.id, id));
}
export async function findSeasonWithSportsSeasons(
id: string
): Promise<SeasonWithSportsSeasons | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.id, id),
with: {
seasonSports: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
}) as SeasonWithSportsSeasons | undefined;
}
/**
* Generate a short, URL-safe invite code
* Uses base62 encoding (alphanumeric) for readability
*/
export function generateInviteCode(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const length = 10;
let result = '';
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += chars[randomValues[i] % chars.length];
}
return result;
}
/**
* Find a season by its invite code
*/
export async function findSeasonByInviteCode(
inviteCode: string
): Promise<Season | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.inviteCode, inviteCode),
});
}
/**
* Regenerate invite code for a season
*/
export async function regenerateInviteCode(seasonId: string): Promise<Season> {
const newInviteCode = generateInviteCode();
return await updateSeason(seasonId, { inviteCode: newInviteCode });
}
export async function countSeasonsByStatus(): Promise<Record<SeasonStatus, number>> {
const db = database();
const rows = await db
.select({ status: schema.seasons.status, count: sql<number>`count(*)::int` })
.from(schema.seasons)
.groupBy(schema.seasons.status);
const result: Record<SeasonStatus, number> = {
pre_draft: 0,
draft: 0,
active: 0,
completed: 0,
};
for (const row of rows) {
if (row.status in result) {
result[row.status as SeasonStatus] = row.count;
}
}
return result;
}