Auto check in all feature Closes #1687

This commit is contained in:
Kalle 2024-03-10 14:42:27 +02:00
parent 6e95763b17
commit 97b97dc815
6 changed files with 117 additions and 69 deletions

View File

@ -387,6 +387,7 @@ export interface TournamentSettings {
teamsPerGroup?: number;
thirdPlaceMatch?: boolean;
isRanked?: boolean;
autoCheckInAll?: boolean;
}
export interface CastedMatchesInfo {

View File

@ -386,6 +386,7 @@ type CreateArgs = Pick<
bracketProgression: TournamentSettings["bracketProgression"] | null;
teamsPerGroup?: number;
thirdPlaceMatch?: boolean;
autoCheckInAll?: boolean;
isRanked?: boolean;
};
export async function create(args: CreateArgs) {
@ -398,6 +399,7 @@ export async function create(args: CreateArgs) {
teamsPerGroup: args.teamsPerGroup,
thirdPlaceMatch: args.thirdPlaceMatch,
isRanked: args.isRanked,
autoCheckInAll: args.autoCheckInAll,
};
tournamentId = (
@ -468,6 +470,7 @@ export async function update(args: UpdateArgs) {
teamsPerGroup: args.teamsPerGroup,
thirdPlaceMatch: args.thirdPlaceMatch,
isRanked: args.isRanked,
autoCheckInAll: args.autoCheckInAll,
};
await trx

View File

@ -80,6 +80,7 @@ export const newCalendarEventActionSchema = z
checkboxValueToBoolean,
z.boolean().nullish(),
),
autoCheckInAll: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
teamsPerGroup: z.coerce
.number()
.min(TOURNAMENT.MIN_GROUP_SIZE)

View File

@ -118,6 +118,7 @@ export const action: ActionFunction = async ({ request }) => {
teamsPerGroup: data.teamsPerGroup ?? undefined,
thirdPlaceMatch: data.thirdPlaceMatch ?? undefined,
isRanked: data.isRanked ?? undefined,
autoCheckInAll: data.autoCheckInAll ?? undefined,
};
validate(
!commonArgs.toToolsEnabled || commonArgs.bracketProgression,
@ -892,19 +893,6 @@ function TournamentFormatSelector() {
</div>
) : null}
{format === "RR_TO_SE" ? (
<div>
<Label htmlFor="thirdPlaceMatch">Third place match</Label>
<Toggle
checked={thirdPlaceMatch}
setChecked={setThirdPlaceMatch}
name="thirdPlaceMatch"
id="thirdPlaceMatch"
tiny
/>
</div>
) : null}
{format === "RR_TO_SE" ? (
<div>
<Label htmlFor="teamsPerGroup">Teams per group</Label>
@ -922,6 +910,20 @@ function TournamentFormatSelector() {
</select>
</div>
) : null}
{format === "RR_TO_SE" ? (
<div>
<Label htmlFor="thirdPlaceMatch">Third place match</Label>
<Toggle
checked={thirdPlaceMatch}
setChecked={setThirdPlaceMatch}
name="thirdPlaceMatch"
id="thirdPlaceMatch"
tiny
/>
</div>
) : null}
{format === "RR_TO_SE" ? (
<FollowUpBrackets teamsPerGroup={teamsPerGroup} />
) : null}
@ -931,6 +933,9 @@ function TournamentFormatSelector() {
function FollowUpBrackets({ teamsPerGroup }: { teamsPerGroup: number }) {
const data = useLoaderData<typeof loader>();
const [autoCheckInAll, setAutoCheckInAll] = React.useState(
data.tournamentCtx?.settings.autoCheckInAll ?? false,
);
const [_brackets, setBrackets] = React.useState<Array<FollowUpBracket>>(
() => {
if (
@ -958,56 +963,76 @@ function FollowUpBrackets({ teamsPerGroup }: { teamsPerGroup: number }) {
const validationErrorMsg = validateFollowUpBrackets(brackets, teamsPerGroup);
return (
<div>
<RequiredHiddenInput
isValid={!validationErrorMsg}
name="followUpBrackets"
value={JSON.stringify(brackets)}
/>
<Label>Follow-up brackets</Label>
<div className="stack lg">
{brackets.map((b, i) => (
<FollowUpBracketInputs
key={i}
teamsPerGroup={teamsPerGroup}
onChange={(newBracket) => {
setBrackets(
brackets.map((oldBracket, j) =>
j === i ? newBracket : oldBracket,
),
);
}}
bracket={b}
nth={i + 1}
<>
{brackets.length > 1 ? (
<div>
<Label htmlFor="autoCheckInAll">
Auto check-in to follow-up brackets
</Label>
<Toggle
checked={autoCheckInAll}
setChecked={setAutoCheckInAll}
name="autoCheckInAll"
id="autoCheckInAll"
tiny
/>
))}
<div className="stack sm horizontal">
<Button
size="tiny"
onClick={() => {
setBrackets([...brackets, { name: "", placements: [] }]);
}}
data-testid="add-bracket"
>
Add bracket
</Button>
<Button
size="tiny"
variant="destructive"
onClick={() => {
setBrackets(brackets.slice(0, -1));
}}
disabled={brackets.length === 1}
>
Remove bracket
</Button>
<FormMessage type="info">
If disabled, the only follow-up bracket with automatic check-in is
the top cut
</FormMessage>
</div>
) : null}
<div>
<RequiredHiddenInput
isValid={!validationErrorMsg}
name="followUpBrackets"
value={JSON.stringify(brackets)}
/>
<Label>Follow-up brackets</Label>
<div className="stack lg">
{brackets.map((b, i) => (
<FollowUpBracketInputs
key={i}
teamsPerGroup={teamsPerGroup}
onChange={(newBracket) => {
setBrackets(
brackets.map((oldBracket, j) =>
j === i ? newBracket : oldBracket,
),
);
}}
bracket={b}
nth={i + 1}
/>
))}
<div className="stack sm horizontal">
<Button
size="tiny"
onClick={() => {
setBrackets([...brackets, { name: "", placements: [] }]);
}}
data-testid="add-bracket"
>
Add bracket
</Button>
<Button
size="tiny"
variant="destructive"
onClick={() => {
setBrackets(brackets.slice(0, -1));
}}
disabled={brackets.length === 1}
>
Remove bracket
</Button>
</div>
{validationErrorMsg ? (
<FormMessage type="error">{validationErrorMsg}</FormMessage>
) : null}
{validationErrorMsg ? (
<FormMessage type="error">{validationErrorMsg}</FormMessage>
) : null}
</div>
</div>
</div>
</>
);
}

View File

@ -31,7 +31,7 @@ import * as TournamentRepository from "~/features/tournament/TournamentRepositor
import { HACKY_isInviteOnlyEvent } from "~/features/tournament/tournament-utils";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import { useVisibilityChange } from "~/hooks/useVisibilityChange";
import { removeDuplicates } from "~/utils/arrays";
import { nullFilledArray, removeDuplicates } from "~/utils/arrays";
import { parseRequestFormData, validate } from "~/utils/remix";
import { assertUnreachable } from "~/utils/types";
import {
@ -113,8 +113,17 @@ export const action: ActionFunction = async ({ params, request }) => {
const finalStageIdx = tournament.brackets.findIndex((b) => b.isFinals);
if (finalStageIdx !== -1) {
const allFollowUpBracketIdxs = nullFilledArray(
tournament.brackets.length,
)
.map((_, i) => i)
// filter out groups stage
.filter((i) => i !== 0);
await TournamentRepository.checkInMany({
bracketIdx: finalStageIdx,
bracketIdxs: tournament.ctx.settings.autoCheckInAll
? allFollowUpBracketIdxs
: [finalStageIdx],
tournamentTeamIds: tournament.ctx.teams
.filter((t) => t.checkIns.length > 0)
.map((t) => t.id),
@ -259,7 +268,7 @@ export default function TournamentBracketsPage() {
) {
return `Teams that get eliminated in the first ${Math.abs(
Math.min(...(bracket.sources ?? []).flatMap((s) => s.placements)),
)} rounds of the losers bracket can play in this bracket (optional)`;
)} rounds of the losers bracket can play in this bracket`;
}
return null;
@ -358,6 +367,13 @@ export default function TournamentBracketsPage() {
{teamsSourceText()}
</div>
) : null}
{bracket.sources &&
bracket.sources.every((s) => !s.placements.includes(1)) &&
!tournament.ctx.settings.autoCheckInAll ? (
<div className="text-center text-sm font-semi-bold text-lighter mt-2 text-warning">
Bracket requires check-in
</div>
) : null}
</div>
) : null}
</div>

View File

@ -333,19 +333,21 @@ export function checkOut({
export function checkInMany({
tournamentTeamIds,
bracketIdx,
bracketIdxs,
}: {
tournamentTeamIds: number[];
bracketIdx: number;
bracketIdxs: number[];
}) {
return db
.insertInto("TournamentTeamCheckIn")
.values(
tournamentTeamIds.map((tournamentTeamId) => ({
checkedInAt: dateToDatabaseTimestamp(new Date()),
tournamentTeamId,
bracketIdx,
})),
tournamentTeamIds.flatMap((tournamentTeamId) =>
bracketIdxs.map((bracketIdx) => ({
checkedInAt: dateToDatabaseTimestamp(new Date()),
tournamentTeamId,
bracketIdx,
})),
),
)
.execute();
}