From 3b343e066e4ce5fbc80823f9a855d3e6b96c48dd Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 12 Mar 2022 11:06:48 +0200 Subject: [PATCH] Admin can edit scores --- app/components/play/MapList.tsx | 33 ++++++-- app/constants.ts | 3 +- app/core/common/permissions.ts | 5 ++ app/models/LFGMatch.server.ts | 46 +++++++++--- app/routes/play/match.$id.tsx | 128 +++++++++++++++++++++----------- prisma/seed/script.ts | 13 ++-- 6 files changed, 160 insertions(+), 68 deletions(-) create mode 100644 app/core/common/permissions.ts diff --git a/app/components/play/MapList.tsx b/app/components/play/MapList.tsx index 7b087d76a..e73c1a04b 100644 --- a/app/components/play/MapList.tsx +++ b/app/components/play/MapList.tsx @@ -4,8 +4,8 @@ import clone from "just-clone"; import { useState } from "react"; import { Form } from "remix"; import { scoreValid } from "~/core/play/validators"; -import { Button } from "../Button"; import { ModeImage } from "../ModeImage"; +import { SubmitButton } from "../SubmitButton"; const NO_RESULT = "NO_RESULT"; @@ -14,14 +14,20 @@ interface MapListProps { name: string; mode: Mode; }[]; + reportedWinnerIds: string[]; canSubmitScore: boolean; groupIds: { our: string; their: string; }; } -export function MapList({ mapList, canSubmitScore, groupIds }: MapListProps) { - const [winners, setWinners] = useState([]); +export function MapList({ + mapList, + reportedWinnerIds, + canSubmitScore, + groupIds, +}: MapListProps) { + const [winners, setWinners] = useState(reportedWinnerIds); const updateWinners = (winnerId: string, index: number) => { const newWinners = clone(winners); @@ -79,7 +85,12 @@ export function MapList({ mapList, canSubmitScore, groupIds }: MapListProps) { ); })} {canSubmitScore && ( - + )} ); @@ -89,6 +100,7 @@ function Submitter({ mapList, winners, groupIds, + isFirstTimeReporting, }: { mapList: MapListProps["mapList"]; winners: string[]; @@ -96,6 +108,7 @@ function Submitter({ our: string; their: string; }; + isFirstTimeReporting: boolean; }) { const warningText = scoreValid(winners, mapList.length) ? undefined @@ -119,9 +132,17 @@ function Submitter({
- +
); diff --git a/app/constants.ts b/app/constants.ts index 371bf4037..355f85a8b 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -2,7 +2,8 @@ import { Ability } from "@prisma/client"; export const DISCORD_URL = "https://discord.gg/sendou"; -export const ADMIN_TEST_UUID = "846e12eb-d373-4002-a0c3-e23077e1c88c"; +export const ADMIN_UUID = "ee2d82dd-624f-4b07-9d8d-ddee1f8fb36f"; + export const ADMIN_TEST_DISCORD_ID = "79237403620945920"; export const ADMIN_TEST_AVATAR = "fcfd65a3bea598905abb9ca25296816b"; export const NZAP_TEST_UUID = "fab649fe-3421-46e9-88cf-d10aa2153821"; diff --git a/app/core/common/permissions.ts b/app/core/common/permissions.ts new file mode 100644 index 000000000..4f0a5c8bf --- /dev/null +++ b/app/core/common/permissions.ts @@ -0,0 +1,5 @@ +import { ADMIN_UUID } from "~/constants"; + +export function isAdmin(userId?: string) { + return userId === ADMIN_UUID; +} diff --git a/app/models/LFGMatch.server.ts b/app/models/LFGMatch.server.ts index 71d71b6da..0c4e3bc5c 100644 --- a/app/models/LFGMatch.server.ts +++ b/app/models/LFGMatch.server.ts @@ -103,12 +103,7 @@ export function recentOfUser(userId: string) { }); } -export async function reportScore({ - UNSAFE_matchId, - UNSAFE_winnerGroupIds, - playerIds, - groupIds, -}: { +interface ReportScoreArgs { UNSAFE_matchId: string; /** Group ID's in order of stages win */ UNSAFE_winnerGroupIds: string[]; @@ -117,7 +112,13 @@ export async function reportScore({ losing: string[]; }; groupIds: string[]; -}) { +} +export async function reportScore({ + UNSAFE_matchId, + UNSAFE_winnerGroupIds, + playerIds, + groupIds, +}: ReportScoreArgs) { const allPlayerIds = [...playerIds.winning, ...playerIds.losing]; const skills = await db.skill.findMany({ where: { userId: { in: allPlayerIds } }, @@ -143,8 +144,32 @@ export async function reportScore({ status: "INACTIVE", }, }), - // https://stackoverflow.com/a/26715934 - db.$executeRawUnsafe(` + insertScores({ UNSAFE_matchId, UNSAFE_winnerGroupIds }), + ]); +} + +export async function overrideScores({ + UNSAFE_matchId, + UNSAFE_winnerGroupIds, +}: Pick) { + return db.$transaction([ + db.lfgGroupMatchStage.updateMany({ + where: { lfgGroupMatchId: UNSAFE_matchId }, + data: { winnerGroupId: null }, + }), + insertScores({ + UNSAFE_matchId, + UNSAFE_winnerGroupIds, + }), + ]); +} + +function insertScores({ + UNSAFE_matchId, + UNSAFE_winnerGroupIds, +}: Pick) { + // https://stackoverflow.com/a/26715934 + return db.$executeRawUnsafe(` update "LfgGroupMatchStage" as lfg set "winnerGroupId" = lfg2.winner_id from (values @@ -153,6 +178,5 @@ export async function reportScore({ ).join(",")} ) as lfg2(lfg_group_match_id, "order", winner_id) where lfg2.lfg_group_match_id = lfg."lfgGroupMatchId" and lfg2.order = lfg.order; - `), - ]); + `); } diff --git a/app/routes/play/match.$id.tsx b/app/routes/play/match.$id.tsx index 6a7b93e80..d037b5f0c 100644 --- a/app/routes/play/match.$id.tsx +++ b/app/routes/play/match.$id.tsx @@ -21,11 +21,13 @@ import { CheckmarkIcon } from "~/components/icons/Checkmark"; import { ModeImage } from "~/components/ModeImage"; import { MapList } from "~/components/play/MapList"; import { DISCORD_URL, LFG_AMOUNT_OF_STAGES_TO_GENERATE } from "~/constants"; +import { isAdmin } from "~/core/common/permissions"; import { groupsToWinningAndLosingPlayerIds, scoresAreIdentical, } from "~/core/play/utils"; import { isGroupAdmin, matchIsUnranked } from "~/core/play/validators"; +import { useUser } from "~/hooks/common"; import * as LFGGroup from "~/models/LFGGroup.server"; import * as LFGMatch from "~/models/LFGMatch.server"; import styles from "~/styles/play-match.css"; @@ -70,7 +72,7 @@ const matchActionSchema = z.union([ _action: z.literal("LOOK_AGAIN"), }), z.object({ - _action: z.literal("REPORT_SCORE"), + _action: z.enum(["REPORT_SCORE", "EDIT_REPORTED_SCORE"]), winnerIds: z.preprocess( safeJSONParse, z @@ -87,7 +89,7 @@ const matchActionSchema = z.union([ }), ]); -type ActionData = { +export type MatchActionData = { error?: "DIFFERENT_SCORE" | "ALREADY_IN_GROUP"; ok?: z.infer["_action"]; }; @@ -96,7 +98,7 @@ export const action: ActionFunction = async ({ request, context, params, -}): Promise => { +}): Promise => { invariant(typeof params.id === "string", "Expected params.id to be string"); const data = await parseRequestFormData({ request, @@ -143,6 +145,16 @@ export const action: ActionFunction = async ({ }); break; } + case "EDIT_REPORTED_SCORE": { + validate(isAdmin(user.id), "Not admin"); + + await LFGMatch.overrideScores({ + UNSAFE_matchId: params.id, + UNSAFE_winnerGroupIds: data.winnerIds, + }); + + return { ok: "EDIT_REPORTED_SCORE" }; + } case "LOOK_AGAIN": { validate(matchIsUnranked(match), "Score reporting required"); await LFGGroup.setInactive(ownGroup.id); @@ -268,19 +280,17 @@ export const loader: LoaderFunction = async ({ params, context }) => { groups, scores, createdAtTimestamp: new Date(match.createdAt).getTime(), - mapList: match.stages - .map(({ stage, winnerGroupId }) => { - const winner = () => { - if (!winnerGroupId) return undefined; + mapList: match.stages.map(({ stage, winnerGroupId }) => { + const winner = () => { + if (!winnerGroupId) return undefined; - return groups[0].id === winnerGroupId ? 0 : 1; - }; - return { - ...stage, - winner: winner(), - }; - }) - .filter((stage) => !scores || typeof stage.winner === "number"), + return groups[0].id === winnerGroupId ? 0 : 1; + }; + return { + ...stage, + winner: winner(), + }; + }), }); }; @@ -288,7 +298,9 @@ export const loader: LoaderFunction = async ({ params, context }) => { export default function LFGMatchPage() { const data = useLoaderData(); const transition = useTransition(); - const actionData = useActionData(); + const actionData = useActionData(); + const user = useUser(); + const [adminEditActive, setAdminEditActive] = React.useState(false); const showPlayAgainSection = () => { if (!data.isCaptain) return false; @@ -302,6 +314,12 @@ export default function LFGMatchPage() { return true; }; + React.useEffect(() => { + if (actionData?.ok === "EDIT_REPORTED_SCORE") { + setAdminEditActive(false); + } + }, [actionData]); + return (
{actionData?.error === "DIFFERENT_SCORE" && ( @@ -388,35 +406,48 @@ export default function LFGMatchPage() {
)} - {data.scores && ( + {data.scores && !adminEditActive && (
- {data.mapList.map((stage) => { - return ( - -
- -
-
- - {stage.name} -
-
- -
-
- ); - })} + {data.mapList + .filter((stage) => typeof stage.winner === "number") + .map((stage) => { + return ( + +
+ +
+
+ + {stage.name} +
+
+ +
+
+ ); + })} +
+ )} + {isAdmin(user?.id) && data.scores && ( +
+
)} @@ -438,9 +469,16 @@ export default function LFGMatchPage() { )} - {!data.scores && data.isRanked && ( + {(!data.scores || adminEditActive) && data.isRanked && ( typeof m.winner === "number") + .map((m) => { + const winnerGroup = data.groups[m.winner as number]; + invariant(winnerGroup, "Unexpected winnerGroup is undefined"); + return winnerGroup.id; + })} canSubmitScore={data.isCaptain} groupIds={{ our: data.groups[0].id, diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts index 695c9000a..1d696b460 100644 --- a/prisma/seed/script.ts +++ b/prisma/seed/script.ts @@ -11,7 +11,7 @@ import { SeedVariations } from "~/utils/schemas"; import { ADMIN_TEST_AVATAR, ADMIN_TEST_DISCORD_ID, - ADMIN_TEST_UUID, + ADMIN_UUID, NZAP_TEST_AVATAR, NZAP_TEST_DISCORD_ID, NZAP_TEST_UUID, @@ -71,7 +71,7 @@ export async function seed(variation?: SeedVariations) { const userIdsInTheSystem = (await prisma.user.findMany()) .map((u) => u.id) - .filter((id) => id !== ADMIN_TEST_UUID && id !== NZAP_TEST_UUID); + .filter((id) => id !== ADMIN_UUID && id !== NZAP_TEST_UUID); const remainingUserIdsForGroups = await lfgGroups(userIdsInTheSystem); if (variation === "match" || variation === "tournament-start") { @@ -88,7 +88,7 @@ export async function seed(variation?: SeedVariations) { async function adminUser() { return prisma.user.create({ data: { - id: ADMIN_TEST_UUID, + id: ADMIN_UUID, discordDiscriminator: "4059", discordId: ADMIN_TEST_DISCORD_ID, discordName: "Sendou", @@ -376,7 +376,7 @@ export async function seed(variation?: SeedVariations) { data: { mu: 30 + i, sigma: 15 - i, - userId: ADMIN_TEST_UUID, + userId: ADMIN_UUID, createdAt: new Date(now + i * 10000), }, }); @@ -447,7 +447,7 @@ export async function seed(variation?: SeedVariations) { const members = [ { - memberId: ADMIN_TEST_UUID, + memberId: ADMIN_UUID, captain: true, }, { memberId: NZAP_TEST_UUID }, @@ -510,6 +510,9 @@ export async function seed(variation?: SeedVariations) { { order: 2, stageId: 40, winnerGroupId: PAST_OUR_GROUP_UUID }, { order: 3, stageId: 60, winnerGroupId: PAST_OUR_GROUP_UUID }, { order: 4, stageId: 101, winnerGroupId: PAST_OUR_GROUP_UUID }, + { order: 5, stageId: 30 }, + { order: 6, stageId: 50 }, + { order: 7, stageId: 90 }, ], }, },