diff --git a/app/db/tables.ts b/app/db/tables.ts index c080f9286..1c7b8bbb4 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -465,7 +465,7 @@ export interface TournamentRound { groupId: number; id: GeneratedAlways; number: number; - stageId: StageId; + stageId: number; } export interface TournamentStage { diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index ae2454422..a1818713b 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -516,3 +516,27 @@ export function setMatchAsCasted({ .execute(); }); } + +export function resetBracket(tournamentStageId: number) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("TournamentMatch") + .where("stageId", "=", tournamentStageId) + .execute(); + + await trx + .deleteFrom("TournamentRound") + .where("stageId", "=", tournamentStageId) + .execute(); + + await trx + .deleteFrom("TournamentGroup") + .where("stageId", "=", tournamentStageId) + .execute(); + + await trx + .deleteFrom("TournamentStage") + .where("id", "=", tournamentStageId) + .execute(); + }); +} diff --git a/app/features/tournament/routes/to.$id.admin.tsx b/app/features/tournament/routes/to.$id.admin.tsx index 3b8a6dc81..bbc80f2af 100644 --- a/app/features/tournament/routes/to.$id.admin.tsx +++ b/app/features/tournament/routes/to.$id.admin.tsx @@ -37,6 +37,7 @@ import { adminActionSchema } from "../tournament-schemas.server"; import { tournamentIdFromParams } from "../tournament-utils"; import { useTournament } from "./to.$id"; import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server"; +import { Input } from "~/components/Input"; export const action: ActionFunction = async ({ request, params }) => { const user = await requireUserId(request); @@ -164,8 +165,6 @@ export const action: ActionFunction = async ({ request, params }) => { }); break; } - // TODO: could also handle the case of admin trying - // to add members from a checked in team case "ADD_MEMBER": { validateIsTournamentOrganizer(); const team = tournament.teamById(data.teamId); @@ -226,6 +225,31 @@ export const action: ActionFunction = async ({ request, params }) => { }); break; } + case "RESET_BRACKET": { + validateIsTournamentOrganizer(); + validate(!tournament.ctx.isFinalized, "Tournament is finalized"); + + const bracketToResetIdx = tournament.brackets.findIndex( + (b) => b.id === data.stageId, + ); + const bracketToReset = tournament.brackets[bracketToResetIdx]; + validate(bracketToReset, "Invalid bracket id"); + validate(!bracketToReset.preview, "Bracket has not started"); + + const inProgressBrackets = tournament.brackets.filter((b) => !b.preview); + validate( + inProgressBrackets.every( + (b) => + !b.sources || + b.sources.every((s) => s.bracketIdx !== bracketToResetIdx), + ), + "Some bracket that sources teams from this bracket has started", + ); + + await TournamentRepository.resetBracket(data.stageId); + + break; + } default: { assertUnreachable(data); } @@ -287,6 +311,8 @@ export default function TournamentAdminPage() { Participant list download + Bracket reset + {isAdmin(user) ? : null} ); @@ -762,3 +788,68 @@ function handleDownload({ document.body.appendChild(element); element.click(); } + +function BracketReset() { + const tournament = useTournament(); + const fetcher = useFetcher(); + const inProgressBrackets = tournament.brackets.filter((b) => !b.preview); + const [_bracketToDelete, setBracketToDelete] = React.useState( + inProgressBrackets[0]?.id, + ); + const [confirmText, setConfirmText] = React.useState(""); + + if (inProgressBrackets.length === 0) { + return
No brackets in progress
; + } + + const bracketToDelete = _bracketToDelete ?? inProgressBrackets[0].id; + + const bracketToDeleteName = inProgressBrackets.find( + (bracket) => bracket.id === bracketToDelete, + )?.name; + + return ( +
+ +
+ + +
+
+ + setConfirmText(e.target.value)} + id="bracket-confirmation" + /> +
+ + Reset + +
+ + Resetting a bracket will delete all the match results in it (but not + other brackets) and reset the bracket to its initial state allowing you + to change participating teams. + +
+ ); +} diff --git a/app/features/tournament/tournament-schemas.server.ts b/app/features/tournament/tournament-schemas.server.ts index 25783ec08..95c957b8a 100644 --- a/app/features/tournament/tournament-schemas.server.ts +++ b/app/features/tournament/tournament-schemas.server.ts @@ -104,6 +104,10 @@ export const adminActionSchema = z.union([ z.array(z.string()), ), }), + z.object({ + _action: _action("RESET_BRACKET"), + stageId: id, + }), ]); export const joinSchema = z.object({ diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index 662eec5f0..dd032016f 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -1,4 +1,4 @@ -import { type Page, test, expect } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; import { ADMIN_DISCORD_ID } from "~/constants"; import { NZAP_TEST_ID } from "~/db/seed/constants"; import { @@ -452,4 +452,41 @@ test.describe("Tournament bracket", () => { await backToBracket(page); await expect(page.getByText("🔴 LIVE")).toBeVisible(); }); + + test("resets bracket", async ({ page }) => { + const tournamentId = 1; + + await seed(page); + await impersonate(page); + + await navigate({ + page, + url: tournamentBracketsPage({ tournamentId }), + }); + + await page.getByTestId("finalize-bracket-button").click(); + + await isNotVisible(page.locator('[data-match-id="1"]')); + await page.locator('[data-match-id="2"]').click(); + await reportResult({ + page, + amountOfMapsToReport: 2, + sidesWithMoreThanFourPlayers: ["last"], + }); + + await page.getByTestId("admin-tab").click(); + await page + .getByLabel('Type bracket name ("Main bracket") to confirm') + .fill("Main bracket"); + await page.getByTestId("reset-bracket-button").click(); + + await page.getByLabel("Action").selectOption("CHECK_IN"); + await page.getByLabel("Team").selectOption("1"); + await submit(page); + + await page.getByTestId("brackets-tab").click(); + await page.getByTestId("finalize-bracket-button").click(); + // bye is gone + await expect(page.locator('[data-match-id="1"]')).toBeVisible(); + }); });