Deleting an event from a season only affects that season's scoring event; the shared canonical tournament and the other linked seasons survive. The old flow never communicated this and offered no way to manage the relationship, so it felt like deleting an event might wipe the whole shared tournament. - deleteScoringEvent is now shared-tournament-aware: it auto-heals the primary window (promotes the earliest remaining window when the primary is removed) and optionally deletes the canonical tournament when the last linked window is removed. Returns a result describing what happened. - Add deleteTournament model helper. - Events page: delete confirmation now explains exactly what a delete does (removed from this season only vs. last window), with an opt-in checkbox to also remove the orphaned shared tournament. - Tournament page: add a per-window "Remove" control on the Linked Sports Seasons card to unlink a season, reusing the auto-heal path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
152 lines
3.8 KiB
TypeScript
152 lines
3.8 KiB
TypeScript
import { eq, and, asc, desc } from "drizzle-orm";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
|
|
export type Tournament = typeof schema.tournaments.$inferSelect;
|
|
export type NewTournament = typeof schema.tournaments.$inferInsert;
|
|
export type TournamentStatus = Tournament["status"];
|
|
|
|
export async function createTournament(
|
|
data: NewTournament
|
|
): Promise<Tournament> {
|
|
const db = database();
|
|
const [tournament] = await db
|
|
.insert(schema.tournaments)
|
|
.values(data)
|
|
.returning();
|
|
return tournament;
|
|
}
|
|
|
|
export async function getTournamentById(
|
|
id: string
|
|
): Promise<Tournament | null> {
|
|
const db = database();
|
|
const results = await db
|
|
.select()
|
|
.from(schema.tournaments)
|
|
.where(eq(schema.tournaments.id, id))
|
|
.limit(1);
|
|
return results[0] ?? null;
|
|
}
|
|
|
|
export async function findTournamentsBySport(
|
|
sportId: string
|
|
): Promise<Tournament[]> {
|
|
const db = database();
|
|
return await db
|
|
.select()
|
|
.from(schema.tournaments)
|
|
.where(eq(schema.tournaments.sportId, sportId))
|
|
.orderBy(asc(schema.tournaments.year), asc(schema.tournaments.startsAt));
|
|
}
|
|
|
|
export async function findTournamentBySportNameYear(
|
|
sportId: string,
|
|
name: string,
|
|
year: number
|
|
): Promise<Tournament | null> {
|
|
const db = database();
|
|
const results = await db
|
|
.select()
|
|
.from(schema.tournaments)
|
|
.where(
|
|
and(
|
|
eq(schema.tournaments.sportId, sportId),
|
|
eq(schema.tournaments.name, name),
|
|
eq(schema.tournaments.year, year)
|
|
)
|
|
)
|
|
.limit(1);
|
|
return results[0] ?? null;
|
|
}
|
|
|
|
export async function upsertTournament(
|
|
data: NewTournament
|
|
): Promise<Tournament> {
|
|
if (!data.sportId || !data.name || !data.year) {
|
|
throw new Error("sportId, name, and year are required for upsert");
|
|
}
|
|
|
|
const existing = await findTournamentBySportNameYear(
|
|
data.sportId,
|
|
data.name,
|
|
data.year
|
|
);
|
|
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
return await createTournament(data);
|
|
}
|
|
|
|
export async function findAllTournamentsGroupedBySport(): Promise<
|
|
Array<{
|
|
sport: { id: string; name: string };
|
|
tournaments: Tournament[];
|
|
}>
|
|
> {
|
|
const db = database();
|
|
|
|
const rows = await db
|
|
.select({
|
|
tournament: schema.tournaments,
|
|
sport: {
|
|
id: schema.sports.id,
|
|
name: schema.sports.name,
|
|
},
|
|
})
|
|
.from(schema.tournaments)
|
|
.innerJoin(schema.sports, eq(schema.tournaments.sportId, schema.sports.id))
|
|
.orderBy(
|
|
asc(schema.sports.name),
|
|
desc(schema.tournaments.year),
|
|
asc(schema.tournaments.startsAt)
|
|
);
|
|
|
|
const groupMap = new Map<
|
|
string,
|
|
{ sport: { id: string; name: string }; tournaments: Tournament[] }
|
|
>();
|
|
|
|
for (const row of rows) {
|
|
const existing = groupMap.get(row.sport.id);
|
|
if (existing) {
|
|
existing.tournaments.push(row.tournament);
|
|
} else {
|
|
groupMap.set(row.sport.id, {
|
|
sport: row.sport,
|
|
tournaments: [row.tournament],
|
|
});
|
|
}
|
|
}
|
|
|
|
return Array.from(groupMap.values());
|
|
}
|
|
|
|
export async function updateTournamentStatus(
|
|
id: string,
|
|
status: TournamentStatus
|
|
): Promise<Tournament> {
|
|
const db = database();
|
|
const [tournament] = await db
|
|
.update(schema.tournaments)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(eq(schema.tournaments.id, id))
|
|
.returning();
|
|
return tournament;
|
|
}
|
|
|
|
/**
|
|
* Delete a canonical tournament. The DB cascades remove its tournament_results;
|
|
* any surviving scoring_events have their tournamentId set to NULL (onDelete:
|
|
* "set null"). Callers should only use this once the last linked window has been
|
|
* removed, so no scoring events are left pointing at it.
|
|
*/
|
|
export async function deleteTournament(
|
|
id: string,
|
|
providedDb?: ReturnType<typeof database>
|
|
): Promise<void> {
|
|
const db = providedDb || database();
|
|
await db.delete(schema.tournaments).where(eq(schema.tournaments.id, id));
|
|
}
|