mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Override tournament bracket destination (#1985)
* Initial * Progress * Done? * Update seeding nth
This commit is contained in:
parent
d34782e720
commit
d3a825bd57
|
|
@ -57,6 +57,7 @@ export function Button(props: ButtonProps) {
|
|||
{icon &&
|
||||
React.cloneElement(icon, {
|
||||
className: clsx("button-icon", { lonely: !children }),
|
||||
title: rest.title,
|
||||
})}
|
||||
{loading && loadingText ? loadingText : children}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
export function EditIcon({ className }: { className?: string }) {
|
||||
export function EditIcon({
|
||||
className,
|
||||
title,
|
||||
}: { className?: string; title?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -6,7 +9,7 @@ export function EditIcon({ className }: { className?: string }) {
|
|||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title>Edit Icon</title>
|
||||
<title>{title ?? "Edit Icon"}</title>
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
|
|
|||
|
|
@ -688,6 +688,13 @@ export interface TournamentOrganizationSeries {
|
|||
showLeaderboard: Generated<number>;
|
||||
}
|
||||
|
||||
export interface TournamentBracketProgressionOverride {
|
||||
sourceBracketIdx: number;
|
||||
destinationBracketIdx: number;
|
||||
tournamentTeamId: number;
|
||||
tournamentId: number;
|
||||
}
|
||||
|
||||
export interface TrustRelationship {
|
||||
trustGiverUserId: number;
|
||||
trustReceiverUserId: number;
|
||||
|
|
@ -923,6 +930,7 @@ export interface DB {
|
|||
TournamentOrganizationMember: TournamentOrganizationMember;
|
||||
TournamentOrganizationBadge: TournamentOrganizationBadge;
|
||||
TournamentOrganizationSeries: TournamentOrganizationSeries;
|
||||
TournamentBracketProgressionOverride: TournamentBracketProgressionOverride;
|
||||
TrustRelationship: TrustRelationship;
|
||||
UnvalidatedUserSubmittedImage: UnvalidatedUserSubmittedImage;
|
||||
UnvalidatedVideo: UnvalidatedVideo;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
import { Link, useFetcher } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { Button } from "../../../../components/Button";
|
||||
import { CheckmarkIcon } from "../../../../components/icons/Checkmark";
|
||||
import { CrossIcon } from "../../../../components/icons/Cross";
|
||||
import { EditIcon } from "../../../../components/icons/Edit";
|
||||
import { logger } from "../../../../utils/logger";
|
||||
import { tournamentTeamPage } from "../../../../utils/urls";
|
||||
import { useUser } from "../../../auth/core/user";
|
||||
import type { Bracket } from "../../core/Bracket";
|
||||
import * as Progression from "../../core/Progression";
|
||||
|
||||
export function PlacementsTable({
|
||||
groupId,
|
||||
bracket,
|
||||
allMatchesFinished,
|
||||
}: {
|
||||
groupId: number;
|
||||
bracket: Bracket;
|
||||
allMatchesFinished: boolean;
|
||||
}) {
|
||||
const user = useUser();
|
||||
|
||||
const _standings = bracket
|
||||
.currentStandings(true)
|
||||
.filter((s) => s.groupId === groupId);
|
||||
|
||||
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
||||
if (cur.group_id !== groupId) return acc;
|
||||
|
||||
if (
|
||||
cur.opponent1?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
||||
!acc.includes(cur.opponent1.id)
|
||||
) {
|
||||
acc.push(cur.opponent1.id);
|
||||
}
|
||||
|
||||
if (
|
||||
cur.opponent2?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
||||
!acc.includes(cur.opponent2.id)
|
||||
) {
|
||||
acc.push(cur.opponent2.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
|
||||
const standings = _standings
|
||||
.concat(
|
||||
missingTeams.map((id) => ({
|
||||
team: bracket.tournament.teamById(id)!,
|
||||
stats: {
|
||||
mapLosses: 0,
|
||||
mapWins: 0,
|
||||
points: 0,
|
||||
setLosses: 0,
|
||||
setWins: 0,
|
||||
winsAgainstTied: 0,
|
||||
lossesAgainstTied: 0,
|
||||
},
|
||||
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
||||
groupId,
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
||||
return a.team.seed - b.team.seed;
|
||||
}
|
||||
|
||||
return a.placement - b.placement;
|
||||
});
|
||||
|
||||
const destinationBracket = (placement: number) =>
|
||||
bracket.tournament.brackets.find(
|
||||
(b) =>
|
||||
b.id !== bracket.id &&
|
||||
b.sources?.some(
|
||||
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
||||
),
|
||||
);
|
||||
|
||||
const possibleDestinationBrackets = Progression.destinationsFromBracketIdx(
|
||||
bracket.idx,
|
||||
bracket.tournament.ctx.settings.bracketProgression,
|
||||
).map((idx) => bracket.tournament.bracketByIdx(idx)!);
|
||||
const canEditDestination = (() => {
|
||||
const allDestinationsPreview = possibleDestinationBrackets.every(
|
||||
(b) => b.preview,
|
||||
);
|
||||
|
||||
return (
|
||||
bracket.tournament.isOrganizer(user) &&
|
||||
allDestinationsPreview &&
|
||||
allMatchesFinished
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<table className="rr__placements-table" cellSpacing={0}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th>
|
||||
<abbr title="Set wins and losses">W/L</abbr>
|
||||
</th>
|
||||
{bracket.type === "round_robin" ? (
|
||||
<th>
|
||||
<abbr title="Wins against tied opponents">TB</abbr>
|
||||
</th>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<th>
|
||||
<abbr title="Losses against tied opponents">TB</abbr>
|
||||
</th>
|
||||
) : null}
|
||||
<th>
|
||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
||||
</th>
|
||||
{bracket.type === "round_robin" ? (
|
||||
<th>
|
||||
<abbr title="Score summed up">Scr</abbr>
|
||||
</th>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed set wins of opponents)">
|
||||
Buch.
|
||||
</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed map wins of opponents)">
|
||||
Buch. (M)
|
||||
</abbr>
|
||||
</th>
|
||||
</>
|
||||
) : null}
|
||||
<th>Seed</th>
|
||||
<th />
|
||||
{canEditDestination ? <th /> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((s, i) => {
|
||||
const stats = s.stats!;
|
||||
if (!stats) {
|
||||
logger.error("No stats for team", s.team);
|
||||
return null;
|
||||
}
|
||||
|
||||
const team = bracket.tournament.teamById(s.team.id);
|
||||
|
||||
const dest = destinationBracket(i + 1);
|
||||
|
||||
const overridenDestination =
|
||||
bracket.tournament.ctx.bracketProgressionOverrides.find(
|
||||
(override) =>
|
||||
override.sourceBracketIdx === bracket.idx &&
|
||||
override.tournamentTeamId === s.team.id,
|
||||
);
|
||||
const overridenDestinationBracket = overridenDestination
|
||||
? bracket.tournament.bracketByIdx(
|
||||
overridenDestination.destinationBracketIdx,
|
||||
)!
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<tr key={s.team.id}>
|
||||
<td>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: bracket.tournament.ctx.id,
|
||||
tournamentTeamId: s.team.id,
|
||||
})}
|
||||
>
|
||||
{s.team.name}{" "}
|
||||
</Link>
|
||||
{s.team.droppedOut ? (
|
||||
<span className="text-warning text-xxxs font-bold">
|
||||
Drop-out
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{stats.setWins}/{stats.setLosses}
|
||||
</span>
|
||||
</td>
|
||||
{bracket.type === "round_robin" ? (
|
||||
<td>
|
||||
<span>{stats.winsAgainstTied}</span>
|
||||
</td>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<td>
|
||||
<span>{(stats.lossesAgainstTied ?? 0) * -1}</span>
|
||||
</td>
|
||||
) : null}
|
||||
<td>
|
||||
<span>
|
||||
{stats.mapWins}/{stats.mapLosses}
|
||||
</span>
|
||||
</td>
|
||||
{bracket.type === "round_robin" ? (
|
||||
<td>
|
||||
<span>{stats.points}</span>
|
||||
</td>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<td>
|
||||
<span>{stats.buchholzSets}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.buchholzMaps}</span>
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
<td>{team?.seed}</td>
|
||||
<EditableDestination
|
||||
key={overridenDestinationBracket?.idx}
|
||||
source={bracket}
|
||||
destination={dest}
|
||||
overridenDestination={overridenDestinationBracket}
|
||||
possibleDestinations={possibleDestinationBrackets}
|
||||
allMatchesFinished={allMatchesFinished}
|
||||
canEditDestination={canEditDestination}
|
||||
tournamentTeamId={s.team.id}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableDestination({
|
||||
source,
|
||||
destination,
|
||||
overridenDestination,
|
||||
possibleDestinations: _possibleDestinations,
|
||||
allMatchesFinished,
|
||||
canEditDestination,
|
||||
tournamentTeamId,
|
||||
}: {
|
||||
source: Bracket;
|
||||
destination?: Bracket;
|
||||
overridenDestination?: Bracket;
|
||||
possibleDestinations: Bracket[];
|
||||
allMatchesFinished: boolean;
|
||||
canEditDestination: boolean;
|
||||
tournamentTeamId: number;
|
||||
}) {
|
||||
const fetcher = useFetcher<any>();
|
||||
const [editingDestination, setEditingDestination] = React.useState(false);
|
||||
const [newDestinationIdx, setNewDestinationIdx] = React.useState<
|
||||
number | null
|
||||
>(overridenDestination?.idx ?? destination?.idx ?? -1);
|
||||
|
||||
const handleSubmit = () => {
|
||||
fetcher.submit(
|
||||
{
|
||||
_action: "OVERRIDE_BRACKET_PROGRESSION",
|
||||
tournamentTeamId,
|
||||
sourceBracketIdx: source.idx,
|
||||
destinationBracketIdx: newDestinationIdx,
|
||||
},
|
||||
{ method: "post", encType: "application/json" },
|
||||
);
|
||||
};
|
||||
|
||||
const possibleDestinations = !destination
|
||||
? (["ELIMINATED", ..._possibleDestinations] as const)
|
||||
: _possibleDestinations;
|
||||
|
||||
if (editingDestination) {
|
||||
return (
|
||||
<>
|
||||
<td>
|
||||
<select
|
||||
value={String(newDestinationIdx)}
|
||||
onChange={(e) => setNewDestinationIdx(Number(e.target.value))}
|
||||
>
|
||||
{possibleDestinations.map((b) => (
|
||||
<option
|
||||
key={b === "ELIMINATED" ? "ELIMINATED" : b.id}
|
||||
value={b === "ELIMINATED" ? -1 : b.idx}
|
||||
>
|
||||
{b === "ELIMINATED" ? "Eliminated" : b.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div className="stack horizontal xs">
|
||||
<Button
|
||||
variant="minimal"
|
||||
title="Save destination"
|
||||
icon={<CheckmarkIcon />}
|
||||
size="tiny"
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
<Button
|
||||
variant="minimal-destructive"
|
||||
title="Cancel"
|
||||
size="tiny"
|
||||
icon={<CrossIcon />}
|
||||
onClick={() => setEditingDestination(false)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{allMatchesFinished &&
|
||||
overridenDestination &&
|
||||
overridenDestination.idx !== destination?.idx ? (
|
||||
<td className="text-theme font-bold">
|
||||
<span>→ {overridenDestination.name}</span>
|
||||
</td>
|
||||
) : destination ? (
|
||||
<td
|
||||
className={clsx({
|
||||
"italic text-lighter": !allMatchesFinished,
|
||||
})}
|
||||
>
|
||||
<span>→ {destination.name}</span>
|
||||
</td>
|
||||
) : (
|
||||
<td />
|
||||
)}
|
||||
{canEditDestination ? (
|
||||
<td>
|
||||
<Button
|
||||
variant="minimal"
|
||||
title="Edit destination"
|
||||
icon={<EditIcon />}
|
||||
size="tiny"
|
||||
onClick={() => setEditingDestination(true)}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { tournamentTeamPage } from "~/utils/urls";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
||||
import { Match } from "./Match";
|
||||
import { PlacementsTable } from "./PlacementsTable";
|
||||
import { RoundHeader } from "./RoundHeader";
|
||||
|
||||
export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
||||
|
|
@ -118,152 +115,3 @@ function getGroups(bracket: BracketType) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
function PlacementsTable({
|
||||
groupId,
|
||||
bracket,
|
||||
allMatchesFinished,
|
||||
}: {
|
||||
groupId: number;
|
||||
bracket: BracketType;
|
||||
allMatchesFinished: boolean;
|
||||
}) {
|
||||
const _standings = bracket
|
||||
.currentStandings(true)
|
||||
.filter((s) => s.groupId === groupId);
|
||||
|
||||
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
||||
if (cur.group_id !== groupId) return acc;
|
||||
|
||||
if (
|
||||
cur.opponent1?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
||||
!acc.includes(cur.opponent1.id)
|
||||
) {
|
||||
acc.push(cur.opponent1.id);
|
||||
}
|
||||
|
||||
if (
|
||||
cur.opponent2?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
||||
!acc.includes(cur.opponent2.id)
|
||||
) {
|
||||
acc.push(cur.opponent2.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
|
||||
const standings = _standings
|
||||
.concat(
|
||||
missingTeams.map((id) => ({
|
||||
team: bracket.tournament.teamById(id)!,
|
||||
stats: {
|
||||
mapLosses: 0,
|
||||
mapWins: 0,
|
||||
points: 0,
|
||||
setLosses: 0,
|
||||
setWins: 0,
|
||||
winsAgainstTied: 0,
|
||||
},
|
||||
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
||||
groupId,
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
||||
return a.team.seed - b.team.seed;
|
||||
}
|
||||
|
||||
return a.placement - b.placement;
|
||||
});
|
||||
|
||||
const destinationBracket = (placement: number) =>
|
||||
bracket.tournament.brackets.find(
|
||||
(b) =>
|
||||
b.id !== bracket.id &&
|
||||
b.sources?.some(
|
||||
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<table className="rr__placements-table" cellSpacing={0}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th>
|
||||
<abbr title="Set wins and losses">W/L</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Wins against tied opponents">TB</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Score summed up">Scr</abbr>
|
||||
</th>
|
||||
<th>Seed</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((s, i) => {
|
||||
const stats = s.stats!;
|
||||
if (!stats) {
|
||||
logger.error("No stats for team", s.team);
|
||||
return null;
|
||||
}
|
||||
|
||||
const team = bracket.tournament.teamById(s.team.id);
|
||||
|
||||
const dest = destinationBracket(i + 1);
|
||||
|
||||
return (
|
||||
<tr key={s.team.id}>
|
||||
<td>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: bracket.tournament.ctx.id,
|
||||
tournamentTeamId: s.team.id,
|
||||
})}
|
||||
>
|
||||
{s.team.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{stats.setWins}/{stats.setLosses}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.winsAgainstTied}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{stats.mapWins}/{stats.mapLosses}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.points}</span>
|
||||
</td>
|
||||
<td>{team?.seed}</td>
|
||||
{dest ? (
|
||||
<td
|
||||
className={clsx({
|
||||
"italic text-lighter": !allMatchesFinished,
|
||||
})}
|
||||
>
|
||||
<span>→ {dest.name}</span>
|
||||
</td>
|
||||
) : (
|
||||
<td />
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Link, useFetcher } from "@remix-run/react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { Button } from "~/components/Button";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
|
|
@ -10,11 +10,10 @@ import {
|
|||
} from "~/features/tournament/routes/to.$id";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { tournamentTeamPage } from "~/utils/urls";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
||||
import { Match } from "./Match";
|
||||
import { PlacementsTable } from "./PlacementsTable";
|
||||
import { RoundHeader } from "./RoundHeader";
|
||||
|
||||
export function SwissBracket({
|
||||
|
|
@ -268,166 +267,3 @@ function getGroups(bracket: BracketType) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
function PlacementsTable({
|
||||
groupId,
|
||||
bracket,
|
||||
allMatchesFinished,
|
||||
}: {
|
||||
groupId: number;
|
||||
bracket: BracketType;
|
||||
allMatchesFinished: boolean;
|
||||
}) {
|
||||
const _standings = bracket
|
||||
.currentStandings(true)
|
||||
.filter((s) => s.groupId === groupId);
|
||||
|
||||
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
||||
if (cur.group_id !== groupId) return acc;
|
||||
|
||||
if (
|
||||
cur.opponent1?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
||||
!acc.includes(cur.opponent1.id)
|
||||
) {
|
||||
acc.push(cur.opponent1.id);
|
||||
}
|
||||
|
||||
if (
|
||||
cur.opponent2?.id &&
|
||||
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
||||
!acc.includes(cur.opponent2.id)
|
||||
) {
|
||||
acc.push(cur.opponent2.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
|
||||
const standings = _standings
|
||||
.concat(
|
||||
missingTeams.map((id) => ({
|
||||
team: bracket.tournament.teamById(id)!,
|
||||
stats: {
|
||||
mapLosses: 0,
|
||||
mapWins: 0,
|
||||
points: 0,
|
||||
setLosses: 0,
|
||||
setWins: 0,
|
||||
winsAgainstTied: 0,
|
||||
lossesAgainstTied: 0,
|
||||
},
|
||||
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
||||
groupId,
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
||||
return a.team.seed - b.team.seed;
|
||||
}
|
||||
|
||||
return a.placement - b.placement;
|
||||
});
|
||||
|
||||
const destinationBracket = (placement: number) =>
|
||||
bracket.tournament.brackets.find(
|
||||
(b) =>
|
||||
b.id !== bracket.id &&
|
||||
b.sources?.some(
|
||||
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<table className="rr__placements-table" cellSpacing={0}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th>
|
||||
<abbr title="Set wins and losses">W/L</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Losses against tied opponents">TB</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed set wins of opponents)">Buch.</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed map wins of opponents)">
|
||||
Buch. (M)
|
||||
</abbr>
|
||||
</th>
|
||||
<th>Seed</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((s, i) => {
|
||||
const stats = s.stats!;
|
||||
if (!stats) {
|
||||
logger.error("No stats for team", s.team);
|
||||
return null;
|
||||
}
|
||||
|
||||
const team = bracket.tournament.teamById(s.team.id);
|
||||
|
||||
const dest = destinationBracket(i + 1);
|
||||
|
||||
return (
|
||||
<tr key={s.team.id}>
|
||||
<td>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: bracket.tournament.ctx.id,
|
||||
tournamentTeamId: s.team.id,
|
||||
})}
|
||||
>
|
||||
{s.team.name}{" "}
|
||||
</Link>
|
||||
{s.team.droppedOut ? (
|
||||
<span className="text-warning text-xxxs font-bold">
|
||||
Drop-out
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{stats.setWins}/{stats.setLosses}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{(stats.lossesAgainstTied ?? 0) * -1}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{stats.mapWins}/{stats.mapLosses}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.buchholzSets}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.buchholzMaps}</span>
|
||||
</td>
|
||||
<td>{team?.seed}</td>
|
||||
{dest ? (
|
||||
<td
|
||||
className={clsx({
|
||||
"italic text-lighter": !allMatchesFinished,
|
||||
})}
|
||||
>
|
||||
<span>→ {dest.name}</span>
|
||||
</td>
|
||||
) : (
|
||||
<td />
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -580,3 +580,26 @@ describe("bracketIdxsForStandings", () => {
|
|||
).toEqual([0]); // missing 1 because it's underground when DE is the source
|
||||
});
|
||||
});
|
||||
|
||||
describe("destinationsFromBracketIdx", () => {
|
||||
it("returns correct destination (one destination)", () => {
|
||||
expect(
|
||||
Progression.destinationsFromBracketIdx(
|
||||
0,
|
||||
progressions.roundRobinToSingleElimination,
|
||||
),
|
||||
).toEqual([1]);
|
||||
});
|
||||
|
||||
it("returns correct destination (many destinations)", () => {
|
||||
expect(
|
||||
Progression.destinationsFromBracketIdx(0, progressions.lowInk),
|
||||
).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("returns an empty array if no destinations", () => {
|
||||
expect(
|
||||
Progression.destinationsFromBracketIdx(0, progressions.singleElimination),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -692,3 +692,22 @@ function bracketsReachableFrom(
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function destinationsFromBracketIdx(
|
||||
sourceBracketIdx: number,
|
||||
progression: ParsedBracket[],
|
||||
): number[] {
|
||||
const destinations: number[] = [];
|
||||
|
||||
for (const [destinationBracketIdx, bracket] of progression.entries()) {
|
||||
if (!bracket.sources) continue;
|
||||
|
||||
for (const source of bracket.sources) {
|
||||
if (source.bracketIdx === sourceBracketIdx) {
|
||||
destinations.push(destinationBracketIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return destinations;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, it, test } from "vitest";
|
||||
import type { Match } from "~/modules/brackets-model";
|
||||
import { Tournament } from "./Tournament";
|
||||
import {
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES,
|
||||
PADDLING_POOL_257,
|
||||
} from "./tests/mocks";
|
||||
import { SWIM_OR_SINK_167 } from "./tests/mocks-sos";
|
||||
|
||||
describe("Follow-up bracket progression", () => {
|
||||
const tournamentPP257 = new Tournament(PADDLING_POOL_257());
|
||||
|
|
@ -178,3 +179,179 @@ describe("Follow-up bracket progression", () => {
|
|||
expect(different, "Amount of different matches is incorrect").toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bracket progression override", () => {
|
||||
it("handles no override", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167(),
|
||||
});
|
||||
|
||||
expect(tournament.brackets[1].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[2].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[3].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[4].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
});
|
||||
|
||||
it("overrides causing the team to go to another bracket", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(
|
||||
tournament.brackets[1].participantTournamentTeamIds.includes(14809),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("overrides causing the team not to go to their original bracket", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(
|
||||
tournament.brackets[2].participantTournamentTeamIds.includes(14809),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it("ignores -1 override (used to indicate no progression)", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: -1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(tournament.brackets[1].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[2].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[3].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
expect(tournament.brackets[4].participantTournamentTeamIds).toHaveLength(
|
||||
11,
|
||||
);
|
||||
});
|
||||
|
||||
it("override teams seeded at the end", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(tournament.brackets[1].seeding?.at(-1)).toBe(14809);
|
||||
});
|
||||
|
||||
it("if redundant override, still in the right bracket", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: 2,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(
|
||||
tournament.brackets[2].participantTournamentTeamIds.includes(14809),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("redundants override does not affect the seed", () => {
|
||||
const tournamentTeamId = 14735;
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167(),
|
||||
});
|
||||
const tournamentWOverride = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
{
|
||||
tournamentTeamId,
|
||||
destinationBracketIdx: 2,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const seedingIdx =
|
||||
tournament.brackets[2].seeding?.indexOf(tournamentTeamId);
|
||||
const seedingIdxWOverride =
|
||||
tournamentWOverride.brackets[2].seeding?.indexOf(tournamentTeamId);
|
||||
|
||||
expect(typeof seedingIdx === "number").toBeTruthy();
|
||||
expect(seedingIdx).toBe(seedingIdxWOverride);
|
||||
});
|
||||
|
||||
// note there is also logic for avoiding replays
|
||||
it("override teams seeded according to their placement in the source bracket", () => {
|
||||
const tournament = new Tournament({
|
||||
...SWIM_OR_SINK_167([
|
||||
// throw these to different brackets to avoid replays
|
||||
{
|
||||
tournamentTeamId: 14657,
|
||||
destinationBracketIdx: 2,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
{
|
||||
tournamentTeamId: 14800,
|
||||
destinationBracketIdx: 2,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
{
|
||||
tournamentTeamId: 14743,
|
||||
destinationBracketIdx: 2,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
// ---
|
||||
{
|
||||
tournamentTeamId: 14737,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
{
|
||||
tournamentTeamId: 14809,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
{
|
||||
tournamentTeamId: 14796,
|
||||
destinationBracketIdx: 1,
|
||||
sourceBracketIdx: 0,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(tournament.brackets[1].seeding?.at(-3)).toBe(14809);
|
||||
expect(tournament.brackets[1].seeding?.at(-2)).toBe(14796);
|
||||
expect(tournament.brackets[1].seeding?.at(-1)).toBe(14737);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export class Tournament {
|
|||
);
|
||||
} else if (type === "swiss") {
|
||||
const { teams, relevantMatchesFinished } = sources
|
||||
? this.resolveTeamsFromSources(sources)
|
||||
? this.resolveTeamsFromSources(sources, bracketIdx)
|
||||
: {
|
||||
teams: this.ctx.teams.map((team) => team.id),
|
||||
relevantMatchesFinished: true,
|
||||
|
|
@ -209,7 +209,7 @@ export class Tournament {
|
|||
);
|
||||
} else {
|
||||
const { teams, relevantMatchesFinished } = sources
|
||||
? this.resolveTeamsFromSources(sources)
|
||||
? this.resolveTeamsFromSources(sources, bracketIdx)
|
||||
: {
|
||||
teams: this.ctx.teams.map((team) => team.id),
|
||||
relevantMatchesFinished: true,
|
||||
|
|
@ -261,23 +261,75 @@ export class Tournament {
|
|||
|
||||
private resolveTeamsFromSources(
|
||||
sources: NonNullable<Progression.ParsedBracket["sources"]>,
|
||||
bracketIdx: number,
|
||||
) {
|
||||
const teams: number[] = [];
|
||||
|
||||
let allRelevantMatchesFinished = true;
|
||||
for (const { bracketIdx, placements } of sources) {
|
||||
const sourceBracket = this.bracketByIdx(bracketIdx);
|
||||
for (const source of sources) {
|
||||
const sourceBracket = this.bracketByIdx(source.bracketIdx);
|
||||
invariant(sourceBracket, "Bracket not found");
|
||||
|
||||
const { teams: sourcedTeams, relevantMatchesFinished } =
|
||||
sourceBracket.source(placements);
|
||||
sourceBracket.source(source.placements);
|
||||
if (!relevantMatchesFinished) {
|
||||
allRelevantMatchesFinished = false;
|
||||
}
|
||||
teams.push(...sourcedTeams);
|
||||
|
||||
const excludedOverridenTeams = sourcedTeams.filter(
|
||||
(teamId) =>
|
||||
!this.ctx.bracketProgressionOverrides.some(
|
||||
(override) =>
|
||||
override.sourceBracketIdx === source.bracketIdx &&
|
||||
override.tournamentTeamId === teamId &&
|
||||
// "no progression" override
|
||||
override.destinationBracketIdx !== -1 &&
|
||||
// redundant override
|
||||
override.destinationBracketIdx !== bracketIdx,
|
||||
),
|
||||
);
|
||||
|
||||
teams.push(...excludedOverridenTeams);
|
||||
}
|
||||
|
||||
return { teams, relevantMatchesFinished: allRelevantMatchesFinished };
|
||||
const teamsFromOverride: { id: number; sourceBracketIdx: number }[] = [];
|
||||
for (const source of sources) {
|
||||
for (const override of this.ctx.bracketProgressionOverrides) {
|
||||
if (override.sourceBracketIdx !== source.bracketIdx) continue;
|
||||
if (override.destinationBracketIdx !== bracketIdx) continue;
|
||||
|
||||
teamsFromOverride.push({
|
||||
id: override.tournamentTeamId,
|
||||
sourceBracketIdx: source.bracketIdx,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const overridesWithoutRepeats = teamsFromOverride
|
||||
.filter(({ id }) => !teams.includes(id))
|
||||
.sort((a, b) => {
|
||||
if (a.sourceBracketIdx !== b.sourceBracketIdx) return 0;
|
||||
|
||||
const bracket = this.bracketByIdx(a.sourceBracketIdx);
|
||||
if (!bracket) return 0;
|
||||
|
||||
const aStanding = bracket.standings.find(
|
||||
(standing) => standing.team.id === a.id,
|
||||
);
|
||||
const bStanding = bracket.standings.find(
|
||||
(standing) => standing.team.id === b.id,
|
||||
);
|
||||
|
||||
if (!aStanding || !bStanding) return 0;
|
||||
|
||||
return aStanding.placement - bStanding.placement;
|
||||
})
|
||||
.map(({ id }) => id);
|
||||
|
||||
return {
|
||||
teams: teams.concat(overridesWithoutRepeats),
|
||||
relevantMatchesFinished: allRelevantMatchesFinished,
|
||||
};
|
||||
}
|
||||
|
||||
private avoidReplaysOfPreviousBracketOpponent(
|
||||
|
|
|
|||
7630
app/features/tournament-bracket/core/tests/mocks-sos.ts
Normal file
7630
app/features/tournament-bracket/core/tests/mocks-sos.ts
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1981,6 +1981,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
organization: null,
|
||||
tags: null,
|
||||
eventId: 1352,
|
||||
bracketProgressionOverrides: [],
|
||||
settings: {
|
||||
bracketProgression: [
|
||||
{
|
||||
|
|
@ -7882,6 +7883,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
organization: null,
|
||||
tags: null,
|
||||
eventId: 1286,
|
||||
bracketProgressionOverrides: [],
|
||||
settings: {
|
||||
bracketProgression: [
|
||||
{
|
||||
|
|
@ -14119,6 +14121,7 @@ export const IN_THE_ZONE_32 = ({
|
|||
organization: null,
|
||||
tags: null,
|
||||
eventId: 1134,
|
||||
bracketProgressionOverrides: [],
|
||||
settings: {
|
||||
bracketProgression: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export const testTournament = ({
|
|||
isFinalized: 0,
|
||||
name: "test",
|
||||
castTwitchAccounts: [],
|
||||
bracketProgressionOverrides: [],
|
||||
subCounts: [],
|
||||
staff: [],
|
||||
tieBreakerMapPool: [],
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
import { currentSeason } from "~/features/mmr/season";
|
||||
import { refreshUserSkills } from "~/features/mmr/tiered.server";
|
||||
import { TOURNAMENT, tournamentIdFromParams } from "~/features/tournament";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
||||
import { createSwissBracketInTransaction } from "~/features/tournament/queries/createSwissBracketInTransaction.server";
|
||||
import { updateRoundMaps } from "~/features/tournament/queries/updateRoundMaps.server";
|
||||
|
|
@ -173,6 +174,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
break;
|
||||
}
|
||||
case "ADVANCE_BRACKET": {
|
||||
validate(tournament.isOrganizer(user));
|
||||
|
||||
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
||||
validate(bracket, "Bracket not found");
|
||||
validate(bracket.type === "swiss", "Can't advance non-swiss bracket");
|
||||
|
|
@ -187,6 +190,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
break;
|
||||
}
|
||||
case "UNADVANCE_BRACKET": {
|
||||
validate(tournament.isOrganizer(user));
|
||||
|
||||
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
||||
validate(bracket, "Bracket not found");
|
||||
validate(bracket.type === "swiss", "Can't unadvance non-swiss bracket");
|
||||
|
|
@ -267,6 +272,33 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "OVERRIDE_BRACKET_PROGRESSION": {
|
||||
validate(tournament.isOrganizer(user));
|
||||
|
||||
const allDestinationBrackets = Progression.destinationsFromBracketIdx(
|
||||
data.sourceBracketIdx,
|
||||
tournament.ctx.settings.bracketProgression,
|
||||
);
|
||||
validate(
|
||||
data.destinationBracketIdx === -1 ||
|
||||
allDestinationBrackets.includes(data.destinationBracketIdx),
|
||||
"Invalid destination bracket",
|
||||
);
|
||||
validate(
|
||||
allDestinationBrackets.every(
|
||||
(bracketIdx) => tournament.bracketByIdx(bracketIdx)!.preview,
|
||||
),
|
||||
"Can't override progression if follow-up brackets are started",
|
||||
);
|
||||
|
||||
await TournamentRepository.overrideTeamBracketProgression({
|
||||
tournamentTeamId: data.tournamentTeamId,
|
||||
sourceBracketIdx: data.sourceBracketIdx,
|
||||
destinationBracketIdx: data.destinationBracketIdx,
|
||||
tournamentId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,12 @@ export const bracketSchema = z.union([
|
|||
_action: _action("BRACKET_CHECK_IN"),
|
||||
bracketIdx,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("OVERRIDE_BRACKET_PROGRESSION"),
|
||||
tournamentTeamId: id,
|
||||
sourceBracketIdx: bracketIdx,
|
||||
destinationBracketIdx: z.union([bracketIdx, z.literal(-1)]),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const matchPageParamsSchema = z.object({ mid: id });
|
||||
|
|
|
|||
|
|
@ -134,6 +134,20 @@ export async function findById(id: number) {
|
|||
.where("TournamentSub.tournamentId", "=", id)
|
||||
.groupBy("TournamentSub.visibility"),
|
||||
).as("subCounts"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TournamentBracketProgressionOverride")
|
||||
.select([
|
||||
"TournamentBracketProgressionOverride.sourceBracketIdx",
|
||||
"TournamentBracketProgressionOverride.destinationBracketIdx",
|
||||
"TournamentBracketProgressionOverride.tournamentTeamId",
|
||||
])
|
||||
.whereRef(
|
||||
"TournamentBracketProgressionOverride.tournamentId",
|
||||
"=",
|
||||
"Tournament.id",
|
||||
),
|
||||
).as("bracketProgressionOverrides"),
|
||||
exists(
|
||||
selectFrom("TournamentResult")
|
||||
.where("TournamentResult.tournamentId", "=", id)
|
||||
|
|
@ -695,6 +709,29 @@ export function updateProgression({
|
|||
});
|
||||
}
|
||||
|
||||
export function overrideTeamBracketProgression({
|
||||
tournamentId,
|
||||
tournamentTeamId,
|
||||
sourceBracketIdx,
|
||||
destinationBracketIdx,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
tournamentTeamId: number;
|
||||
sourceBracketIdx: number;
|
||||
destinationBracketIdx: number;
|
||||
}) {
|
||||
// set in migration: unique("sourceBracketIdx", "tournamentTeamId") on conflict replace
|
||||
return db
|
||||
.insertInto("TournamentBracketProgressionOverride")
|
||||
.values({
|
||||
tournamentId,
|
||||
tournamentTeamId,
|
||||
sourceBracketIdx,
|
||||
destinationBracketIdx,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function updateTeamName({
|
||||
tournamentTeamId,
|
||||
name,
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
21
migrations/076-progression-override.js
Normal file
21
migrations/076-progression-override.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/*sql*/ `
|
||||
create table "TournamentBracketProgressionOverride" (
|
||||
"sourceBracketIdx" integer not null,
|
||||
"destinationBracketIdx" integer not null,
|
||||
"tournamentTeamId" integer not null,
|
||||
"tournamentId" integer not null,
|
||||
unique("sourceBracketIdx", "tournamentTeamId") on conflict replace,
|
||||
foreign key ("tournamentTeamId") references "TournamentTeam"("id") on delete cascade,
|
||||
foreign key ("tournamentId") references "Tournament"("id") on delete cascade
|
||||
) strict
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
/*sql*/ `create index tournament_bracket_progression_override_tournament_id on "TournamentBracketProgressionOverride"("tournamentId")`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user