2025-10-28 23:50:50 -07:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2026-03-07 21:59:29 -08:00
|
|
|
import { eq, and, desc, asc, gte, lte } from "drizzle-orm";
|
2025-10-28 23:50:50 -07:00
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
export type EventType = "playoff_game" | "major_tournament" | "final_standings" | "schedule_event";
|
|
|
|
|
|
|
|
|
|
export function getEventTypeLabel(eventType: string): string {
|
|
|
|
|
switch (eventType) {
|
|
|
|
|
case "playoff_game": return "Bracket";
|
|
|
|
|
case "major_tournament": return "Major Tournament";
|
|
|
|
|
case "final_standings": return "Final Standings";
|
|
|
|
|
case "schedule_event": return "Non-Scoring";
|
|
|
|
|
default: return eventType;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 23:50:50 -07:00
|
|
|
|
|
|
|
|
export interface CreateScoringEventData {
|
|
|
|
|
sportsSeasonId: string;
|
|
|
|
|
name: string;
|
|
|
|
|
eventDate?: Date;
|
|
|
|
|
eventType: EventType;
|
|
|
|
|
isQualifyingEvent?: boolean;
|
2025-11-03 13:16:37 -08:00
|
|
|
// Template system fields (Phase 2.6)
|
|
|
|
|
bracketTemplateId?: string;
|
|
|
|
|
scoringStartsAtRound?: string;
|
2025-10-28 23:50:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UpdateScoringEventData {
|
|
|
|
|
name?: string;
|
|
|
|
|
eventDate?: Date;
|
|
|
|
|
isComplete?: boolean;
|
|
|
|
|
completedAt?: Date;
|
2025-11-03 13:16:37 -08:00
|
|
|
bracketTemplateId?: string;
|
|
|
|
|
scoringStartsAtRound?: string;
|
2025-10-28 23:50:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new scoring event
|
|
|
|
|
*/
|
|
|
|
|
export async function createScoringEvent(
|
|
|
|
|
data: CreateScoringEventData,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
const [event] = await db
|
|
|
|
|
.insert(schema.scoringEvents)
|
|
|
|
|
.values({
|
|
|
|
|
sportsSeasonId: data.sportsSeasonId,
|
|
|
|
|
name: data.name,
|
|
|
|
|
eventDate: data.eventDate ? data.eventDate.toISOString().split('T')[0] : undefined,
|
|
|
|
|
eventType: data.eventType,
|
|
|
|
|
isQualifyingEvent: data.isQualifyingEvent || false,
|
2025-11-03 13:16:37 -08:00
|
|
|
bracketTemplateId: data.bracketTemplateId,
|
|
|
|
|
scoringStartsAtRound: data.scoringStartsAtRound,
|
2025-10-28 23:50:50 -07:00
|
|
|
isComplete: false,
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
return event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a scoring event by ID
|
|
|
|
|
*/
|
|
|
|
|
export async function getScoringEventById(
|
|
|
|
|
eventId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return await db.query.scoringEvents.findFirst({
|
|
|
|
|
where: eq(schema.scoringEvents.id, eventId),
|
|
|
|
|
with: {
|
|
|
|
|
sportsSeason: {
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all scoring events for a sports season
|
|
|
|
|
*/
|
|
|
|
|
export async function getScoringEventsForSportsSeason(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return await db.query.scoringEvents.findMany({
|
|
|
|
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.createdAt)],
|
|
|
|
|
with: {
|
|
|
|
|
eventResults: {
|
|
|
|
|
with: {
|
|
|
|
|
participant: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get incomplete scoring events for a sports season
|
|
|
|
|
*/
|
|
|
|
|
export async function getIncompleteScoringEvents(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return await db.query.scoringEvents.findMany({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
eq(schema.scoringEvents.isComplete, false)
|
|
|
|
|
),
|
|
|
|
|
orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.createdAt)],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get qualifying events for a sports season
|
|
|
|
|
*/
|
|
|
|
|
export async function getQualifyingEvents(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return await db.query.scoringEvents.findMany({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
eq(schema.scoringEvents.isQualifyingEvent, true)
|
|
|
|
|
),
|
|
|
|
|
orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.createdAt)],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a scoring event
|
|
|
|
|
*/
|
|
|
|
|
export async function updateScoringEvent(
|
|
|
|
|
eventId: string,
|
|
|
|
|
data: UpdateScoringEventData,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
const updateData: any = { updatedAt: new Date() };
|
|
|
|
|
|
|
|
|
|
if (data.name !== undefined) updateData.name = data.name;
|
|
|
|
|
if (data.eventDate !== undefined) {
|
|
|
|
|
updateData.eventDate = data.eventDate.toISOString().split('T')[0];
|
|
|
|
|
}
|
|
|
|
|
if (data.isComplete !== undefined) updateData.isComplete = data.isComplete;
|
|
|
|
|
if (data.completedAt !== undefined) updateData.completedAt = data.completedAt;
|
2025-11-03 13:16:37 -08:00
|
|
|
if (data.bracketTemplateId !== undefined) updateData.bracketTemplateId = data.bracketTemplateId;
|
|
|
|
|
if (data.scoringStartsAtRound !== undefined) updateData.scoringStartsAtRound = data.scoringStartsAtRound;
|
2025-10-28 23:50:50 -07:00
|
|
|
|
|
|
|
|
const [updated] = await db
|
|
|
|
|
.update(schema.scoringEvents)
|
|
|
|
|
.set(updateData)
|
|
|
|
|
.where(eq(schema.scoringEvents.id, eventId))
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
return updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark a scoring event as complete
|
|
|
|
|
*/
|
|
|
|
|
export async function completeScoringEvent(
|
|
|
|
|
eventId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return await updateScoringEvent(
|
|
|
|
|
eventId,
|
|
|
|
|
{
|
|
|
|
|
isComplete: true,
|
|
|
|
|
completedAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
db
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a scoring event
|
|
|
|
|
*/
|
|
|
|
|
export async function deleteScoringEvent(
|
|
|
|
|
eventId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
await db.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if all events for a sports season are complete
|
|
|
|
|
*/
|
|
|
|
|
export async function areAllEventsComplete(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
const incompleteEvents = await getIncompleteScoringEvents(sportsSeasonId, db);
|
|
|
|
|
return incompleteEvents.length === 0;
|
|
|
|
|
}
|
2026-03-07 21:59:29 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get upcoming (not yet complete) scoring events for a sports season,
|
|
|
|
|
* ordered by eventDate ascending (soonest first).
|
|
|
|
|
* Events without a date are included last.
|
|
|
|
|
*/
|
|
|
|
|
export async function getUpcomingScoringEvents(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
limit = 5,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return db.query.scoringEvents.findMany({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
eq(schema.scoringEvents.isComplete, false)
|
|
|
|
|
),
|
|
|
|
|
orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.createdAt)],
|
|
|
|
|
limit,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the single next upcoming event for a sports season (for card display).
|
|
|
|
|
*/
|
|
|
|
|
export async function getNextScoringEvent(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const upcoming = await getUpcomingScoringEvents(sportsSeasonId, 1, providedDb);
|
|
|
|
|
return upcoming[0] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get recently completed scoring events for a sports season,
|
|
|
|
|
* ordered by completedAt descending (most recent first).
|
|
|
|
|
*/
|
|
|
|
|
export async function getRecentCompletedEvents(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
limit = 3,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
return db.query.scoringEvents.findMany({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
eq(schema.scoringEvents.isComplete, true)
|
|
|
|
|
),
|
|
|
|
|
orderBy: [desc(schema.scoringEvents.completedAt), desc(schema.scoringEvents.eventDate)],
|
|
|
|
|
limit,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk create scoring events for a sports season.
|
|
|
|
|
* Returns created events in order.
|
|
|
|
|
*/
|
|
|
|
|
export async function bulkCreateScoringEvents(
|
|
|
|
|
sportsSeasonId: string,
|
|
|
|
|
events: Array<{ name: string; eventDate?: Date; eventType: EventType; isQualifyingEvent?: boolean }>,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
|
|
|
|
) {
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
if (events.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const rows = events.map((e) => ({
|
|
|
|
|
sportsSeasonId,
|
|
|
|
|
name: e.name,
|
|
|
|
|
eventDate: e.eventDate ? e.eventDate.toISOString().split("T")[0] : undefined,
|
|
|
|
|
eventType: e.eventType,
|
|
|
|
|
isQualifyingEvent: e.isQualifyingEvent ?? false,
|
|
|
|
|
isComplete: false,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return db.insert(schema.scoringEvents).values(rows).returning();
|
|
|
|
|
}
|