Allow reporting weapons after tournament has finalized

This commit is contained in:
Kalle 2026-05-11 20:04:41 +03:00
parent a5e69273c7
commit ee6e92967b
6 changed files with 91 additions and 3 deletions

View File

@ -10,6 +10,7 @@ import {
import {
modesIncluded,
sortTeamsBySeeding,
tournamentInWeaponReportingWindow,
tournamentIsRanked,
} from "~/features/tournament/tournament-utils";
import type * as Progression from "~/features/tournament-bracket/core/Progression";
@ -895,6 +896,16 @@ export class Tournament {
return this.registrationClosesAt > new Date();
}
/** Can participants submit/undo their own weapon reports right now?
* Always open while the tournament is running; once finalized it stays open only for tournaments
* whose startTime is inside the current-season-plus-adjacent-off-season window. */
get weaponReportingOpen() {
if (!this.ctx.isFinalized) return true;
return tournamentInWeaponReportingWindow({
tournamentStartTime: this.ctx.startTime,
});
}
/**
* Does this tournament have autonomous subs feature enabled?
* If enabled, teams can add members to their roster while tournament is in progress without having to request the organizer to do it.

View File

@ -774,7 +774,10 @@ export const action: ActionFunction = async ({ params, request }) => {
(p) => p.id === user.id,
);
errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized");
errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized");
errorToastIfFalsy(
tournament.weaponReportingOpen,
"Weapon reporting is closed",
);
await ReportedWeaponRepository.upsertOneTournament({
tournamentMatchId: matchId,
@ -790,7 +793,10 @@ export const action: ActionFunction = async ({ params, request }) => {
(p) => p.id === user.id,
);
errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized");
errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized");
errorToastIfFalsy(
tournament.weaponReportingOpen,
"Weapon reporting is closed",
);
await ReportedWeaponRepository.deleteByUserMapIndexTournament({
tournamentMatchId: matchId,

View File

@ -38,6 +38,7 @@ export function TournamentMatchActionTab({
const weaponReport = useTournamentWeaponReport({
data,
viewerUserId: user?.id,
weaponReportingOpen: tournament.weaponReportingOpen,
});
if (!currentMap) {
@ -152,9 +153,11 @@ export function TournamentMatchActionTab({
function useTournamentWeaponReport({
data,
viewerUserId,
weaponReportingOpen,
}: {
data: TournamentMatchLoaderData;
viewerUserId: number | undefined;
weaponReportingOpen: boolean;
}) {
const playOrderMaps = (data.mapList ?? []).filter(
(m) => !m.bannedByTournamentTeamId,
@ -177,6 +180,7 @@ function useTournamentWeaponReport({
});
if (viewerUserId === undefined) return null;
if (!weaponReportingOpen) return null;
const isParticipant = data.match.players.some((p) => p.id === viewerUserId);
if (!isParticipant) return null;

View File

@ -100,7 +100,7 @@ export function MatchPageProvider({
user,
}),
canReportWeapons:
isParticipant && !tournament.ctx.isFinalized && hasReportedMaps,
isParticipant && tournament.weaponReportingOpen && hasReportedMaps,
canJoin: data.canJoin,
hasCurrentMap: Boolean(currentMap),
hasMissingActiveRoster: teamsMissingActiveRoster.length > 0,

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { CastedMatchesInfo } from "~/db/tables";
import * as Seasons from "../mmr/core/Seasons";
import type { ParsedBracket } from "../tournament-bracket/core/Progression";
import {
compareTeamsForOrdering,
@ -7,6 +8,7 @@ import {
getBracketProgressionLabel,
sortTeamsBySeeding,
type TeamForOrdering,
tournamentInWeaponReportingWindow,
updatedCastedMatchesInfo,
} from "./tournament-utils";
@ -642,3 +644,43 @@ describe("updatedCastedMatchesInfo", () => {
});
});
});
describe("tournamentInWeaponReportingWindow", () => {
const anchorSeason = Seasons.list[2]!;
const previousSeason = Seasons.list[1]!;
const dateInside = (range: { starts: Date; ends: Date }) =>
new Date((range.starts.getTime() + range.ends.getTime()) / 2);
const inSeasonNow = dateInside(anchorSeason);
const offSeasonNow = new Date(
(previousSeason.ends.getTime() + anchorSeason.starts.getTime()) / 2,
);
it("allows tournaments started in the off-season before current season (in-season)", () => {
expect(
tournamentInWeaponReportingWindow({
tournamentStartTime: offSeasonNow,
now: inSeasonNow,
}),
).toBe(true);
});
it("rejects tournaments started before the previous season ended (in-season)", () => {
expect(
tournamentInWeaponReportingWindow({
tournamentStartTime: dateInside(previousSeason),
now: inSeasonNow,
}),
).toBe(false);
});
it("allows tournaments started during the previous season (off-season)", () => {
expect(
tournamentInWeaponReportingWindow({
tournamentStartTime: dateInside(previousSeason),
now: offSeasonNow,
}),
).toBe(true);
});
});

View File

@ -159,6 +159,31 @@ export function tournamentIsRanked({
return isSetAsRanked ?? true;
}
/**
* Whether a tournament's startTime falls inside the active weapon-reporting window
* for late (post-finalization) reporting.
*
* - In-season: window is `(previousSeason.ends, now]` current season plus the off-season immediately before it.
* - Off-season: window is `[previousSeason.starts, now]` previous full season plus the current off-season.
*/
export function tournamentInWeaponReportingWindow({
tournamentStartTime,
now = new Date(),
}: {
tournamentStartTime: Date;
now?: Date;
}) {
const previousSeason = Seasons.previous(now);
if (!previousSeason) return true;
const currentSeason = Seasons.current(now);
const windowStart = currentSeason
? previousSeason.ends
: previousSeason.starts;
return tournamentStartTime > windowStart;
}
export function resolveLeagueRoundStartDate(
tournament: TournamentClass,
roundId: number,