claude/shared-tournaments-events-ux-6z5jkt #125

Merged
chrisp merged 2 commits from claude/shared-tournaments-events-ux-6z5jkt into main 2026-07-02 18:30:20 +00:00
3 changed files with 49 additions and 12 deletions
Showing only changes of commit ec89e3eb89 - Show all commits

View file

@ -119,6 +119,24 @@ describe("deleteScoringEvent", () => {
expect(result.deletedTournament).toBe(false);
});
it("does not promote a primary when a golf-style tournament (no primary) loses a window", async () => {
// Golf-style shared majors intentionally have no primary window — every
// linked event is isPrimary=false. Deleting one must not flip a sibling
// into a primary, which would change its scoring/guard behavior.
const { db } = makeSharedDb({
deletedEvent: { tournamentId: "tournament-1", isPrimary: false },
remaining: [
{ id: "event-2", isPrimary: false },
{ id: "event-3", isPrimary: false },
],
});
const result = await deleteScoringEvent("event-1", db);
expect(result.promotedPrimaryId).toBeNull();
expect(result.remainingWindows).toBe(2);
});
it("promotes the earliest remaining window when the primary window is deleted", async () => {
const { db } = makeSharedDb({
deletedEvent: { tournamentId: "tournament-1", isPrimary: true },

View file

@ -305,11 +305,13 @@ export async function deleteScoringEvent(
remainingWindows = remaining.length;
if (remaining.length > 0) {
// Auto-heal: if the deleted event was the primary window (or no sibling is
// flagged primary anymore), promote the earliest remaining window so the
// shared major stays scorable and fan-out stays deterministic.
const hasPrimary = remaining.some((e) => e.isPrimary);
if (event.isPrimary || !hasPrimary) {
// Auto-heal: if the deleted event was the primary window, promote the
// earliest remaining window so the shared major stays scorable and
// fan-out stays deterministic. Only heal when we actually removed the
// primary — golf-style majors intentionally have no primary (scored on the
// canonical tournament page), and event.isPrimary is false for them, so
// they are left untouched.
if (event.isPrimary) {
await setPrimaryEvent(remaining[0].id, db);
promotedPrimaryId = remaining[0].id;
}
@ -1126,6 +1128,24 @@ export async function getSportsSeasonsByTournament(tournamentId: string) {
});
}
/**
* Count the distinct sports-season windows linked to a tournament. Cheaper than
* getSportsSeasonsByTournament when only the count is needed (no relations).
*/
export async function countWindowsByTournament(
tournamentId: string,
providedDb?: ReturnType<typeof database>
): Promise<number> {
const db = providedDb || database();
const [row] = await db
.select({
count: sql<number>`count(distinct ${schema.scoringEvents.sportsSeasonId})::int`,
})
.from(schema.scoringEvents)
.where(eq(schema.scoringEvents.tournamentId, tournamentId));
return row?.count ?? 0;
}
/**
* The primary scoring event for a tournament the single window where the admin
* builds the bracket/stages and scoring happens; its results fan out to siblings.

View file

@ -8,7 +8,7 @@ import {
deleteScoringEvent,
bulkCreateScoringEvents,
ensurePrimaryEvent,
getSportsSeasonsByTournament,
countWindowsByTournament,
type CreateScoringEventData,
} from "~/models/scoring-event";
import { isBracketMajor } from "~/lib/event-utils";
@ -66,15 +66,14 @@ export async function loader({ params }: Route.LoaderArgs) {
const sharedInfo = new Map<string, { name: string; windowCount: number }>();
await Promise.all(
[...linkedTournamentIds].map(async (tid) => {
const [tournament, windows] = await Promise.all([
allTournamentsForSport.find((t) => t.id === tid)
? Promise.resolve(allTournamentsForSport.find((t) => t.id === tid)!)
: getTournamentById(tid),
getSportsSeasonsByTournament(tid),
const cached = allTournamentsForSport.find((t) => t.id === tid);
const [tournament, windowCount] = await Promise.all([
cached ?? getTournamentById(tid),
countWindowsByTournament(tid),
]);
sharedInfo.set(tid, {
name: tournament?.name ?? "shared tournament",
windowCount: windows.length,
windowCount,
});
})
);