Admin can edit scores

This commit is contained in:
Kalle
2022-03-12 11:06:48 +02:00
parent fe6e8a50f2
commit 3b343e066e
6 changed files with 160 additions and 68 deletions

View File

@@ -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<string[]>([]);
export function MapList({
mapList,
reportedWinnerIds,
canSubmitScore,
groupIds,
}: MapListProps) {
const [winners, setWinners] = useState<string[]>(reportedWinnerIds);
const updateWinners = (winnerId: string, index: number) => {
const newWinners = clone(winners);
@@ -79,7 +85,12 @@ export function MapList({ mapList, canSubmitScore, groupIds }: MapListProps) {
);
})}
{canSubmitScore && (
<Submitter mapList={mapList} winners={winners} groupIds={groupIds} />
<Submitter
mapList={mapList}
winners={winners}
groupIds={groupIds}
isFirstTimeReporting={reportedWinnerIds.length === 0}
/>
)}
</ol>
);
@@ -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({
<div className="play-match__score-submit-button">
<Form method="post">
<input type="hidden" name="winnerIds" value={JSON.stringify(winners)} />
<Button type="submit" name="_action" value="REPORT_SCORE">
<SubmitButton
type="submit"
name="_action"
value={isFirstTimeReporting ? "REPORT_SCORE" : "EDIT_REPORTED_SCORE"}
actionType={
isFirstTimeReporting ? "REPORT_SCORE" : "EDIT_REPORTED_SCORE"
}
loadingText="Submitting..."
>
Submit {score.join("-")}
</Button>
</SubmitButton>
</Form>
</div>
);

View File

@@ -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";

View File

@@ -0,0 +1,5 @@
import { ADMIN_UUID } from "~/constants";
export function isAdmin(userId?: string) {
return userId === ADMIN_UUID;
}

View File

@@ -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<ReportScoreArgs, "UNSAFE_matchId" | "UNSAFE_winnerGroupIds">) {
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<ReportScoreArgs, "UNSAFE_matchId" | "UNSAFE_winnerGroupIds">) {
// 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;
`),
]);
`);
}

View File

@@ -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<typeof matchActionSchema>["_action"];
};
@@ -96,7 +98,7 @@ export const action: ActionFunction = async ({
request,
context,
params,
}): Promise<ActionData | Response> => {
}): Promise<MatchActionData | Response> => {
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<LFGMatchLoaderData>();
const transition = useTransition();
const actionData = useActionData<ActionData>();
const actionData = useActionData<MatchActionData>();
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 (
<div>
{actionData?.error === "DIFFERENT_SCORE" && (
@@ -388,35 +406,48 @@ export default function LFGMatchPage() {
</div>
</Form>
)}
{data.scores && (
{data.scores && !adminEditActive && (
<div className="play-match__played-map-list">
{data.mapList.map((stage) => {
return (
<React.Fragment key={`${stage.name}-${stage.mode}`}>
<div
className={clsx("play-match__checkmark", "left", {
invisible: stage.winner !== 0,
})}
>
<CheckmarkIcon />
</div>
<div className="play-match__played-stage">
<ModeImage
className="play-match__played-mode"
mode={stage.mode}
/>
{stage.name}
</div>
<div
className={clsx("play-match__checkmark", {
invisible: stage.winner !== 1,
})}
>
<CheckmarkIcon />
</div>
</React.Fragment>
);
})}
{data.mapList
.filter((stage) => typeof stage.winner === "number")
.map((stage) => {
return (
<React.Fragment key={`${stage.name}-${stage.mode}`}>
<div
className={clsx("play-match__checkmark", "left", {
invisible: stage.winner !== 0,
})}
>
<CheckmarkIcon />
</div>
<div className="play-match__played-stage">
<ModeImage
className="play-match__played-mode"
mode={stage.mode}
/>
{stage.name}
</div>
<div
className={clsx("play-match__checkmark", {
invisible: stage.winner !== 1,
})}
>
<CheckmarkIcon />
</div>
</React.Fragment>
);
})}
</div>
)}
{isAdmin(user?.id) && data.scores && (
<div className="flex justify-center mt-4">
<Button
tiny
variant={adminEditActive ? "destructive" : undefined}
onClick={() => setAdminEditActive((isActive) => !isActive)}
>
{adminEditActive ? "Cancel" : "Edit"}
</Button>
</div>
)}
</div>
@@ -438,9 +469,16 @@ export default function LFGMatchPage() {
</Form>
</div>
)}
{!data.scores && data.isRanked && (
{(!data.scores || adminEditActive) && data.isRanked && (
<MapList
mapList={data.mapList}
reportedWinnerIds={data.mapList
.filter((m) => 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,

View File

@@ -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 },
],
},
},