From c802faf151f2d951e39a0852988d114247cf7845 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 1 May 2026 16:29:19 +0300 Subject: [PATCH] Tournament weapon report --- .../match-page/WeaponReporter.module.css | 6 + app/components/match-page/WeaponReporter.tsx | 28 ++-- app/db/tables.ts | 6 + .../ReportedWeaponRepository.server.ts | 143 ++++++++++++++++++ .../seasonReportedWeaponsByUserId.server.ts | 35 ----- .../tournament-bracket-schemas.server.ts | 10 ++ .../actions/to.$id.matches.$mid.server.ts | 32 ++++ .../components/TournamentMatchActionTab.tsx | 94 +++++++++++- .../components/TournamentMatchTabs.tsx | 37 ++++- .../loaders/to.$id.matches.$mid.server.ts | 5 + .../loaders/u.$identifier.seasons.server.ts | 4 +- db-test.sqlite3 | Bin 1253376 -> 1265664 bytes migrations/134-match-page.js | 47 ++++++ 13 files changed, 392 insertions(+), 55 deletions(-) delete mode 100644 app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts diff --git a/app/components/match-page/WeaponReporter.module.css b/app/components/match-page/WeaponReporter.module.css index adc15e82c..ea167ec24 100644 --- a/app/components/match-page/WeaponReporter.module.css +++ b/app/components/match-page/WeaponReporter.module.css @@ -90,6 +90,12 @@ position: relative; } +.rootStandalone { + margin-block-start: calc(-1 * var(--s-6)); + min-height: 200px; + justify-content: center; +} + .collapseButton { position: absolute; top: var(--s-2); diff --git a/app/components/match-page/WeaponReporter.tsx b/app/components/match-page/WeaponReporter.tsx index d426d7c1f..8ceb414f2 100644 --- a/app/components/match-page/WeaponReporter.tsx +++ b/app/components/match-page/WeaponReporter.tsx @@ -27,6 +27,7 @@ export interface WeaponReporterProps { onSubmit: (weaponSplId: MainWeaponId) => void; onUndo: () => void; isSubmitting?: boolean; + standalone?: boolean; } // xxx: on sendouq all weapons report different / component tab..? or not? check usage @@ -37,6 +38,7 @@ export function WeaponReporter({ onSubmit, onUndo, isSubmitting, + standalone, }: WeaponReporterProps) { const { t } = useTranslation(["q", "game-misc", "common"]); const user = useUser(); @@ -62,7 +64,7 @@ export function WeaponReporter({ ); }; - if (!isOpen) { + if (!isOpen && !standalone) { return (
- } - onPress={() => handleToggle(false)} - className={styles.collapseButton} - aria-label={t("q:match.actions.reportWeapons")} - /> +
+ {standalone ? null : ( + } + onPress={() => handleToggle(false)} + className={styles.collapseButton} + aria-label={t("q:match.actions.reportWeapons")} + /> + )} {pastReported.length > 0 ? (
{pastReported.map((weaponId, i) => ( diff --git a/app/db/tables.ts b/app/db/tables.ts index 2679d14e1..b00787aa3 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -453,8 +453,14 @@ export interface PlusVotingResult { passedVoting: DBBoolean; } +// xxx: unify SendouQ side to also key on (matchId, mapIndex). Replace +// groupMatchMapId with groupMatchId, make mapIndex non-null, and have a +// single CHECK that exactly one of groupMatchId / tournamentMatchId is set. +// Backfill (groupMatchId, mapIndex) from GroupMatchMap.matchId/index. export interface ReportedWeapon { groupMatchMapId: number | null; + tournamentMatchId: number | null; + mapIndex: number | null; userId: number; weaponSplId: MainWeaponId; } diff --git a/app/features/sendouq-match/ReportedWeaponRepository.server.ts b/app/features/sendouq-match/ReportedWeaponRepository.server.ts index 4423274c3..df1db2221 100644 --- a/app/features/sendouq-match/ReportedWeaponRepository.server.ts +++ b/app/features/sendouq-match/ReportedWeaponRepository.server.ts @@ -1,6 +1,9 @@ import type { NotNull, Transaction } from "kysely"; import { db } from "~/db/sql"; import type { DB, TablesInsertable } from "~/db/tables"; +import * as Seasons from "~/features/mmr/core/Seasons"; +import type { MainWeaponId } from "~/modules/in-game-lists/types"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; export function createMany( weapons: TablesInsertable["ReportedWeapon"][], @@ -106,3 +109,143 @@ export async function findByMatchId(matchId: number) { return rows; } + +export async function upsertOneTournament({ + tournamentMatchId, + mapIndex, + userId, + weaponSplId, +}: TablesInsertable["ReportedWeapon"] & { + tournamentMatchId: number; + mapIndex: number; +}) { + await db + .deleteFrom("ReportedWeapon") + .where("tournamentMatchId", "=", tournamentMatchId) + .where("mapIndex", "=", mapIndex) + .where("userId", "=", userId) + .execute(); + + await db + .insertInto("ReportedWeapon") + .values({ tournamentMatchId, mapIndex, userId, weaponSplId }) + .execute(); +} + +export async function deleteByUserMapIndexTournament({ + tournamentMatchId, + userId, + mapIndex, +}: { + tournamentMatchId: number; + userId: number; + mapIndex: number; +}) { + await db + .deleteFrom("ReportedWeapon") + .where("tournamentMatchId", "=", tournamentMatchId) + .where("mapIndex", "=", mapIndex) + .where("userId", "=", userId) + .execute(); +} + +export async function findByTournamentMatchId(matchId: number) { + const rows = await db + .selectFrom("ReportedWeapon") + .select([ + "ReportedWeapon.tournamentMatchId", + "ReportedWeapon.mapIndex", + "ReportedWeapon.weaponSplId", + "ReportedWeapon.userId", + ]) + .where("ReportedWeapon.tournamentMatchId", "=", matchId) + .orderBy("ReportedWeapon.mapIndex", "asc") + .orderBy("ReportedWeapon.userId", "asc") + .$narrowType<{ tournamentMatchId: NotNull; mapIndex: NotNull }>() + .execute(); + + if (rows.length === 0) return null; + + return rows; +} + +/** + * Aggregates a user's reported weapons across both SendouQ matches and + * finalized tournaments that fall within the given season's date range. + */ +export async function seasonReportedWeaponsByUserId({ + userId, + season, +}: { + userId: number; + season: number; +}): Promise> { + const { starts, ends } = Seasons.nthToDateRange(season); + const startsTs = dateToDatabaseTimestamp(starts); + const endsTs = dateToDatabaseTimestamp(ends); + + const sendouqWeapons = db + .selectFrom("ReportedWeapon") + .innerJoin( + "GroupMatchMap", + "GroupMatchMap.id", + "ReportedWeapon.groupMatchMapId", + ) + .innerJoin("GroupMatch", "GroupMatch.id", "GroupMatchMap.matchId") + .select(({ fn }) => [ + "ReportedWeapon.weaponSplId", + fn.countAll().as("count"), + ]) + .where("ReportedWeapon.userId", "=", userId) + .where("GroupMatch.createdAt", ">=", startsTs) + .where("GroupMatch.createdAt", "<=", endsTs) + .groupBy("ReportedWeapon.weaponSplId"); + + const tournamentWeapons = db + .selectFrom("ReportedWeapon") + .innerJoin( + "TournamentMatch", + "TournamentMatch.id", + "ReportedWeapon.tournamentMatchId", + ) + .innerJoin( + "TournamentStage", + "TournamentStage.id", + "TournamentMatch.stageId", + ) + .innerJoin("Tournament", "Tournament.id", "TournamentStage.tournamentId") + .innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id") + .innerJoin( + (eb) => + eb + .selectFrom("CalendarEventDate") + .select(({ fn }) => [ + "CalendarEventDate.eventId", + fn.min("CalendarEventDate.startTime").as("startTime"), + ]) + .groupBy("CalendarEventDate.eventId") + .as("EventStartTime"), + (join) => join.onRef("EventStartTime.eventId", "=", "CalendarEvent.id"), + ) + .select(({ fn }) => [ + "ReportedWeapon.weaponSplId", + fn.countAll().as("count"), + ]) + .where("ReportedWeapon.userId", "=", userId) + .where("Tournament.isFinalized", "=", 1) + .where("EventStartTime.startTime", ">=", startsTs) + .where("EventStartTime.startTime", "<=", endsTs) + .groupBy("ReportedWeapon.weaponSplId"); + + const rows = await db + .selectFrom(sendouqWeapons.unionAll(tournamentWeapons).as("merged")) + .select(({ fn }) => [ + "merged.weaponSplId", + fn.sum("merged.count").as("count"), + ]) + .groupBy("merged.weaponSplId") + .orderBy("count", "desc") + .execute(); + + return rows; +} diff --git a/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts b/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts deleted file mode 100644 index 89642ac27..000000000 --- a/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { sql } from "~/db/sql"; -import * as Seasons from "~/features/mmr/core/Seasons"; -import type { MainWeaponId } from "~/modules/in-game-lists/types"; -import { dateToDatabaseTimestamp } from "~/utils/dates"; - -const stm = sql.prepare(/* sql */ ` - select - "ReportedWeapon"."weaponSplId", - count(*) as "count" - from - "ReportedWeapon" - left join "GroupMatchMap" on "GroupMatchMap"."id" = "ReportedWeapon"."groupMatchMapId" - left join "GroupMatch" on "GroupMatch"."id" = "GroupMatchMap"."matchId" - where - "ReportedWeapon"."userId" = @userId - and "GroupMatch"."createdAt" between @starts and @ends - group by "ReportedWeapon"."weaponSplId" - order by "count" desc -`); - -export function seasonReportedWeaponsByUserId({ - userId, - season, -}: { - userId: number; - season: number; -}) { - const { starts, ends } = Seasons.nthToDateRange(season); - - return stm.all({ - userId, - starts: dateToDatabaseTimestamp(starts), - ends: dateToDatabaseTimestamp(ends), - }) as Array<{ weaponSplId: MainWeaponId; count: number }>; -} diff --git a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts index 6bb6f7c78..f9ca57d89 100644 --- a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts +++ b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts @@ -9,6 +9,7 @@ import { numericEnum, safeJSONParse, stageId, + weaponSplId, } from "~/utils/zod"; import { TOURNAMENT } from "../tournament/tournament-constants"; import * as PickBan from "./core/PickBan"; @@ -102,6 +103,15 @@ export const matchSchema = z.union([ _action: _action("END_SET"), winnerTeamId: z.preprocess(nullLiteraltoNull, id.nullable()), }), + z.object({ + _action: _action("REPORT_WEAPON"), + weaponSplId, + mapIndex: z.coerce.number().int().nonnegative(), + }), + z.object({ + _action: _action("UNDO_WEAPON_REPORT"), + mapIndex: z.coerce.number().int().nonnegative(), + }), ]); export const bracketIdx = z.coerce.number().int().min(0).max(100); diff --git a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts index c0df3b420..6d31a4853 100644 --- a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts @@ -3,6 +3,7 @@ import { sql } from "~/db/sql"; import { TournamentMatchStatus } from "~/db/tables"; import { requireUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; +import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import { endDroppedTeamMatches } from "~/features/tournament/tournament-utils.server"; @@ -736,6 +737,37 @@ export const action: ActionFunction = async ({ params, request }) => { break; } + case "REPORT_WEAPON": { + const isMemberOfATeamInTheMatch = match.players.some( + (p) => p.id === user.id, + ); + errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized"); + errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized"); + + await ReportedWeaponRepository.upsertOneTournament({ + tournamentMatchId: matchId, + mapIndex: data.mapIndex, + userId: user.id, + weaponSplId: data.weaponSplId, + }); + + break; + } + case "UNDO_WEAPON_REPORT": { + const isMemberOfATeamInTheMatch = match.players.some( + (p) => p.id === user.id, + ); + errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized"); + errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized"); + + await ReportedWeaponRepository.deleteByUserMapIndexTournament({ + tournamentMatchId: matchId, + userId: user.id, + mapIndex: data.mapIndex, + }); + + break; + } default: { assertUnreachable(data); } diff --git a/app/features/tournament-match/components/TournamentMatchActionTab.tsx b/app/features/tournament-match/components/TournamentMatchActionTab.tsx index d7f3ff947..64b86d4c7 100644 --- a/app/features/tournament-match/components/TournamentMatchActionTab.tsx +++ b/app/features/tournament-match/components/TournamentMatchActionTab.tsx @@ -3,9 +3,21 @@ import { Undo2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useFetcher } from "react-router"; import { SendouButton } from "~/components/elements/Button"; +import { SendouTabPanel } from "~/components/elements/Tabs"; import { MatchActionTab } from "~/components/match-page/MatchActionTab"; +import { TAB_KEYS } from "~/components/match-page/MatchTabs"; +import { + WeaponReporter, + type WeaponReporterProps, +} from "~/components/match-page/WeaponReporter"; +import { useUser } from "~/features/auth/core/user"; +import { useRecentlyReportedWeapons } from "~/features/sendouq/q-hooks"; import { useTournament } from "~/features/tournament/routes/to.$id"; -import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; +import type { + MainWeaponId, + ModeShort, + StageId, +} from "~/modules/in-game-lists/types"; import { databaseTimestampToJavascriptTimestamp } from "~/utils/dates"; import type { CommonUser } from "~/utils/kysely.server"; import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server"; @@ -17,14 +29,28 @@ export function TournamentMatchActionTab({ ownTeamId, }: { data: TournamentMatchLoaderData; - currentMap: { stageId: StageId; mode: ModeShort }; + currentMap?: { stageId: StageId; mode: ModeShort }; ownTeamId: number; }) { const { t } = useTranslation(["q"]); const tournament = useTournament(); + const user = useUser(); const reportFetcher = useFetcher(); const undoFetcher = useFetcher(); + const weaponReport = useTournamentWeaponReport({ + data, + viewerUserId: user?.id, + }); + + if (!currentMap) { + return ( + + {weaponReport ? : null} + + ); + } + const opponentOneId = data.match.opponentOne!.id!; const opponentTwoId = data.match.opponentTwo!.id!; @@ -129,10 +155,74 @@ export function TournamentMatchActionTab({ {t("q:match.undoReport")} } + weaponReport={weaponReport ?? undefined} /> ); } +function useTournamentWeaponReport({ + data, + viewerUserId, +}: { + data: TournamentMatchLoaderData; + viewerUserId: number | undefined; +}): WeaponReporterProps | null { + const weaponFetcher = useFetcher(); + const { recentlyReportedWeapons, addRecentlyReportedWeapon } = + useRecentlyReportedWeapons(); + + if (viewerUserId === undefined) return null; + + const isParticipant = data.match.players.some((p) => p.id === viewerUserId); + if (!isParticipant) return null; + + const playOrderMaps = (data.mapList ?? []).filter( + (m) => !m.bannedByTournamentTeamId, + ); + const reportedCount = data.results.length; + const weaponReportMaps = playOrderMaps + .slice(0, reportedCount + 1) + .map((m) => ({ stageId: m.stageId, mode: m.mode })); + + if (weaponReportMaps.length === 0) return null; + + const pastReported: MainWeaponId[] = data.reportedWeapons + ? data.reportedWeapons + .filter((w) => w.userId === viewerUserId) + .map((w) => w.weaponSplId) + : []; + + return { + maps: weaponReportMaps, + pastReported, + quickSelectWeaponIds: recentlyReportedWeapons, + isSubmitting: weaponFetcher.state !== "idle", + onSubmit: (weaponSplId) => { + addRecentlyReportedWeapon(weaponSplId); + const mapIndex = pastReported.length; + weaponFetcher.submit( + { + _action: "REPORT_WEAPON", + weaponSplId: String(weaponSplId), + mapIndex: String(mapIndex), + }, + { method: "post" }, + ); + }, + onUndo: () => { + const mapIndex = pastReported.length - 1; + if (mapIndex < 0) return; + weaponFetcher.submit( + { + _action: "UNDO_WEAPON_REPORT", + mapIndex: String(mapIndex), + }, + { method: "post" }, + ); + }, + }; +} + function buildSetEndingData({ teams, scores, diff --git a/app/features/tournament-match/components/TournamentMatchTabs.tsx b/app/features/tournament-match/components/TournamentMatchTabs.tsx index 83edbeb07..82f26d9f8 100644 --- a/app/features/tournament-match/components/TournamentMatchTabs.tsx +++ b/app/features/tournament-match/components/TournamentMatchTabs.tsx @@ -86,9 +86,14 @@ export function TournamentMatchTabs({ const hasReportedMaps = data.results.length > 0; const hasPickBanEvents = data.pickBanEventCount > 0; + const isParticipant = data.match.players.some((p) => p.id === user?.id); + const canReportWeapons = + isParticipant && !tournament.ctx.isFinalized && hasReportedMaps; + const tabs = resolveVisibleTabs({ matchIsOver: data.matchIsOver, canReportScore, + canReportWeapons, canJoin: data.canJoin, hasCurrentMap: Boolean(currentMap), hasMissingActiveRoster, @@ -136,13 +141,13 @@ export function TournamentMatchTabs({ teams={pickBanTeams} turnOfResult={turnOfResult} /> - ) : currentMap ? ( + ) : ( - ) : null + ) ) : null} {tabs.includes("admin") ? : null} @@ -192,10 +197,24 @@ function resolveTimelineMaps( customUrl: u.customUrl, })); - return data.results.map((result) => { + return data.results.map((result, mapIndex) => { const hasPoints = result.opponentOnePoints !== null && result.opponentTwoPoints !== null; + const alphaRoster = resolveRoster(result.participants, opponentOneId); + const bravoRoster = resolveRoster(result.participants, opponentTwoId); + + const weaponFor = (userId: number) => + data.reportedWeapons?.find( + (w) => w.mapIndex === mapIndex && w.userId === userId, + )?.weaponSplId ?? null; + + const alphaWeapons = alphaRoster.map((u) => weaponFor(u.id)); + const bravoWeapons = bravoRoster.map((u) => weaponFor(u.id)); + const hasAnyWeapon = + alphaWeapons.some((w) => w !== null) || + bravoWeapons.some((w) => w !== null); + return { stageId: result.stageId, mode: result.mode, @@ -205,9 +224,12 @@ function resolveTimelineMaps( ? ("ALPHA" as const) : ("BRAVO" as const), rosters: { - alpha: resolveRoster(result.participants, opponentOneId), - bravo: resolveRoster(result.participants, opponentTwoId), + alpha: alphaRoster, + bravo: bravoRoster, }, + weapons: hasAnyWeapon + ? { alpha: alphaWeapons, bravo: bravoWeapons } + : undefined, points: hasPoints ? ([result.opponentOnePoints, result.opponentTwoPoints] as [ number, @@ -485,6 +507,7 @@ function TournamentMatchRosterTab({ function resolveVisibleTabs({ matchIsOver, canReportScore, + canReportWeapons, canJoin, hasCurrentMap, hasMissingActiveRoster, @@ -496,6 +519,7 @@ function resolveVisibleTabs({ }: { matchIsOver: boolean; canReportScore: boolean; + canReportWeapons: boolean; canJoin: boolean; hasCurrentMap: boolean; hasMissingActiveRoster: boolean; @@ -517,7 +541,8 @@ function resolveVisibleTabs({ if ( !leagueRoundLocked && (isPickBanStep || - (canReportScore && hasCurrentMap && !hasMissingActiveRoster)) + (canReportScore && hasCurrentMap && !hasMissingActiveRoster) || + canReportWeapons) ) { tabs.push("action"); } diff --git a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts index 4fa7c2388..b93b192dc 100644 --- a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts @@ -4,6 +4,7 @@ import { getUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import { chatAccessible } from "~/features/chat/chat-utils"; import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server"; +import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import { isLeagueRoundLocked } from "~/features/tournament/tournament-utils"; @@ -53,6 +54,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { const results = findResultsByMatchId(matchId); + const reportedWeapons = + await ReportedWeaponRepository.findByTournamentMatchId(matchId); + const matchIsOver = match.opponentOne?.result === "win" || match.opponentTwo?.result === "win"; @@ -219,6 +223,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { return { match: hasPermsToSeeChat ? match : { ...match, chatCode: undefined }, results, + reportedWeapons, mapList, matchIsOver, endedEarly, diff --git a/app/features/user-page/loaders/u.$identifier.seasons.server.ts b/app/features/user-page/loaders/u.$identifier.seasons.server.ts index bf92700b1..1262dae74 100644 --- a/app/features/user-page/loaders/u.$identifier.seasons.server.ts +++ b/app/features/user-page/loaders/u.$identifier.seasons.server.ts @@ -4,10 +4,10 @@ import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepos import * as SkillRepository from "~/features/mmr/SkillRepository.server"; import { userSkills as _userSkills } from "~/features/mmr/tiered.server"; import { seasonMapWinrateByUserId } from "~/features/sendouq/queries/seasonMapWinrateByUserId.server"; -import { seasonReportedWeaponsByUserId } from "~/features/sendouq/queries/seasonReportedWeaponsByUserId.server"; import { seasonSetWinrateByUserId } from "~/features/sendouq/queries/seasonSetWinrateByUserId.server"; import { seasonStagesByUserId } from "~/features/sendouq/queries/seasonStagesByUserId.server"; import { seasonsMatesEnemiesByUserId } from "~/features/sendouq/queries/seasonsMatesEnemiesByUserId.server"; +import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server"; import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import type { SerializeFrom } from "~/utils/remix"; @@ -91,7 +91,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { : null, weapons: info === "weapons" - ? seasonReportedWeaponsByUserId({ season, userId: user.id }) + ? await ReportedWeaponRepository.seasonReportedWeaponsByUserId({ season, userId: user.id }) : null, players: info === "enemies" || info === "mates" diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 54cd492f131fae504afd64db5156576664bcd526..f12b1b373a6628e5bc8bb83ae4dafe09b84c0cab 100644 GIT binary patch delta 6072 zcmeHLZEO=|9KUwAja}KEc5iLhuDx}%TgMLW+I540GBid}UVUL~Y|PRg<6!HJ_QuN; z1~ZPt54_dnNiYh|L@^PVf-Hm$F-Cz8it#0Y(N83R5rZF85(4Pcjt%aAJN)9u-R?ku?ejx`auN1O?a;Ok`cu~Vc7eOhZ$RASq?0cN1rU&_2 zfm;*dq*zqktmF6YBIwikA}2`koOFsqv$$|Xkv584Tuvyum39|b&QiJJpG{LjkPp?4 zR_2l3^cEKfRKn$TH0342q`aG?L_#Nsv&C$UO3WLgDL)Ylj9L@*1RYif$p4d8I4w6> zD*?|(Ru`aLw8DzeHb3C0x0M6RI-3(vk~X+ZKDWVz_`9tX1nW-+Tf_fHu#ekesW;oF z0S7*`!_a=P!_W*Ru9>QAv*k494)VboZev8JLW@i6y($@Z8BLWEnENmN~t!hy$tJThG`V zrO?>PQWw~1a?^DZ0-Pd~SMJc$HPz*BQcVG$X;hNu)<~Pl`O&2#>0#1o;b%XBC%MZ7 zJsok`fv4GNdweYy33rIGXjt#CGPz6YRGwC5($vf#&)0C0(!;F|N85FGlomHU2m0OY zGL^PzMw+T1+Jr}H@*2{snoq52Ns+?pBki?Y?wz;X1~QCfWRO9Vkx51t8QEk^AtPt# zmYZSrcuikYQ;k&=`T{+hH_V)4l1zQh-YM6!vgz!MuZ&fre?z`O<^U-PWxR#YpJJ5M zuUrXkd;R*nivuym{|ZQ_fM+cN`(( z@G;BM8)%@RfzStdexQNxA2jt3PVFDeJu#S)0$n-Yu|i|!(-o);+^TFz-Kq!#14OzLirmR5 zYsF4c#|UcDgtwpx0R)GYCIot8{GOKdT@$JT*~@8003YE{vnB?q8Sy~y{bp1SdT%+^ zdkX^3b;aeF+#S>D_3^!C1n?H-S0aF%@yjbwB`~l)ZGbfQ>DwrR7j<$rvJ!y~9ba3C zYCr^)Et*-<+M0gFAY8m{B58Yg~jMc`HmSFAz+?cwHC zs2ZqGu0j>HstH}uLQ`CjpDl2SNL=dVV)7=Pl>MRAnNh&DC}FC!by)A4yPoS zn|6l!#B8hIZ`@Um-6Kqy(GbK>j4(bN9bvr1EO|T{HP~>(Ts(z<;03A6y7U*mbjIMq z4W~^x`0CFl2R?Y5Veq9pmLhy*gn0(fIBL$vx!-3phDG?sUztzguDcc*|8&Qahrj(T zYdYTjQjQs)JZhebUp>w6xS|aay<1( z?B9QMOpdG5@#uzx`}NiAxcD;I3r~OJJfm(ftOv*)&DsONTmzn1{xe*s&Dv`YX0 delta 5357 zcmeHL*>4*~7~dqDq*=$AIHw&S>s(HpWSZIak)u#0>H`8wNf656Ty+yCY!18Fo1;Of zwJKbR2ijt!m5MkNBn}Coa5bvh2oQpj2ZV$`QU3ub52!pKMMO{-Z={m_HXB}ew6)`h zPxIX~-}n3G!kbTA`26G*-YhZ?emUD8w%sW;`?2VE3u3nz=oW+NGD{tx^jd6yGGpNZ zC2gti(#UD)XBnT5bP_B_!V7pI91YJz;zwr_A`yd_X!fS>S!#hiXIT@V^y?@u=qNYJ z+PXE8TOVhcfRA+eaUv3#lA<#>F(=K>8<@eDL)TB0w*oU#<#mEaMcpGTQ%_Y8d?qn> z&Oq{=awm|JS0MmOZv_UF=PKNQ@@@sRa9g*Bt%O`cr8l6l*>jv_T74ua;OR*$MW-Sw z26kVrgf?weIzWhzsUZ$@;_##_Ek$rVmXHlI%~v&dYE1EVv5ep&f#6j<#Kg1*z@c|9c6j|Hdn1U^t9F>AEjx_ku8ynCVFhyf9S%@v zD%CUhG?D7t4j)2Fs$4T(Cv_kvGSp&{GP7Mp`I%H4!B^R`G(`5se7OMJ45Ko5Z zBhjg_QL{4fHMF1XYnjdYEYt2Iw5Lh(Dn1jQnlY>>qde>bDc;?$k=BsQGEG#PaHyol z9Q9U{%&nGWDXeX27kBteJA4jZxpd{x#nP2eR{>pxbRD6qXk&-xxGRkm50vJ%Fz8G4 zSjjDOMe&ct51YQ?zTwhbchS`&cMA&H!o06HJnP&Wro6@EQ%dr8i>U10Rz=#ipaSNDiw(&qH>{i7-P|>|dySN?kv{dGto4x8x+%3@oLV|Y*B2gL7PtP1YUMPJ-2q17J zIiyqb=@6;~FDU0SFDL@V`;&_aIktf1XCey`10`r!=iZ~k2uy2=Jd6MiR^HN+z8^*{ zAbLe11ZE*65YqV|g%AOZZ--Dlz}c}3&K3lM&&FfX*kaTG*DLo!2+URr89`u5R!)zg zW}skBRX~Bf`tn3)LxY%Yjv#=>m7hnDA9&C_syiiPqdGsY>34lMiok5F{5h)2Qg94) z0FA3-x?P`)>5TYg41pIUrD+_2$xaE4qc$KuJ&u}!ngiW5$}-qTx&)kB@I&+!DR0UDm3Kt}=T@&p1eY0B*hXxf^)XRW74ofGK5koi6$Z0RMIly0!Z^4DiJ qI;j+Ks|?ygFQclGYb9RuJ0{6g!>xX7={wC`THUp5tnQYj8vg+||Ilgx diff --git a/migrations/134-match-page.js b/migrations/134-match-page.js index d77e663ef..9d6b64f96 100644 --- a/migrations/134-match-page.js +++ b/migrations/134-match-page.js @@ -135,6 +135,53 @@ export function up(db) { /* sql */ `create index group_match_continue_vote_group_id on "GroupMatchContinueVote"("groupId")`, ).run(); + db.prepare( + /* sql */ ` + create table "ReportedWeapon_new" ( + "groupMatchMapId" integer, + "tournamentMatchId" integer, + "mapIndex" integer, + "weaponSplId" integer not null, + "userId" integer not null, + foreign key ("groupMatchMapId") references "GroupMatchMap"("id") on delete restrict, + foreign key ("tournamentMatchId") references "TournamentMatch"("id") on delete cascade, + foreign key ("userId") references "User"("id") on delete restrict, + unique("groupMatchMapId", "userId") on conflict rollback, + unique("tournamentMatchId", "mapIndex", "userId") on conflict rollback, + check ( + ("groupMatchMapId" is not null and "tournamentMatchId" is null and "mapIndex" is null) + or + ("groupMatchMapId" is null and "tournamentMatchId" is not null and "mapIndex" is not null) + ) + ) strict + `, + ).run(); + + db.prepare( + /* sql */ ` + insert into "ReportedWeapon_new" ( + "groupMatchMapId", "tournamentMatchId", "mapIndex", "weaponSplId", "userId" + ) + select "groupMatchMapId", null, null, "weaponSplId", "userId" + from "ReportedWeapon" + `, + ).run(); + + db.prepare(/* sql */ `drop table "ReportedWeapon"`).run(); + db.prepare( + /* sql */ `alter table "ReportedWeapon_new" rename to "ReportedWeapon"`, + ).run(); + + db.prepare( + /* sql */ `create index reported_weapon_group_match_map_id on "ReportedWeapon"("groupMatchMapId")`, + ).run(); + db.prepare( + /* sql */ `create index reported_weapon_tournament_match_id on "ReportedWeapon"("tournamentMatchId")`, + ).run(); + db.prepare( + /* sql */ `create index reported_weapon_user_id on "ReportedWeapon"("userId")`, + ).run(); + db.pragma("foreign_key_check"); })();