From 478f5ea9b9f14fe6f5ae327fd41c2c5e96fe82f3 Mon Sep 17 00:00:00 2001 From: Michal Masrna <38047051+michalmasrna1@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:21:06 +0200 Subject: [PATCH] Judging results calculation (#62) * feat(judging): improve judging manager * feat(judging): improve judging results * added back button to judging results * Split teams into two groups based on their assigned judging slots + displaying table codes in the results * changed the judging threshold time to 12:50 * sorted the time slots by the starttime in judge management * Implemented the judging logic * removed unused imports + lint * fixed comment * divide the team score by the number of Verdicts they've received --------- Co-authored-by: Matej Tarca Co-authored-by: Michal Masrna --- .../Dashboard/scenes/Judging/Judging.tsx | 4 +- .../JudgingManagerJudgeTimesheet.tsx | 88 ++++++----- .../scenes/JudgingResults/JudgingResults.tsx | 14 +- .../dashboard/judging/createTeamJudging.ts | 17 ++ .../dashboard/judging/getJudgingResults.ts | 148 ++++++++++++++++-- 5 files changed, 220 insertions(+), 51 deletions(-) diff --git a/src/scenes/Dashboard/scenes/Judging/Judging.tsx b/src/scenes/Dashboard/scenes/Judging/Judging.tsx index ab1a869..c19780a 100644 --- a/src/scenes/Dashboard/scenes/Judging/Judging.tsx +++ b/src/scenes/Dashboard/scenes/Judging/Judging.tsx @@ -20,7 +20,7 @@ const Judging = async ({ hackathonId }: JudgingManagerProps) => { {session?.isAdmin && ( - <> +
- +
)} { return (
- {judge.teamJudgings.map((teamJudging) => ( -
- - {dateToTimeString(teamJudging.judgingSlot.startTime)} -{" "} - {dateToTimeString(teamJudging.judgingSlot.endTime)} - - {teamJudging.team ? ( - <> - {teamJudging.team.name} - { - if (answer) { - await callServerAction(deleteTeamJudging, { - teamJudgingId: teamJudging.team?.teamJudgingId as number, - }); - } - }} - > - - - - ) : ( - ({ - value: team.teamId.toString(), - label: team.nameAndTable, - }))} - /> - )} -
- ))} + {judge.teamJudgings + .sort((teamJudgingA, teamJudgingB) => { + return ( + teamJudgingA.judgingSlot.startTime.getTime() - + teamJudgingB.judgingSlot.startTime.getTime() + ); + }) + .map((teamJudging) => ( +
+ + {dateToTimeString(teamJudging.judgingSlot.startTime)} -{" "} + {dateToTimeString(teamJudging.judgingSlot.endTime)} + + {teamJudging.team ? ( + <> + {teamJudging.team.name} + {" - "} + {teamJudging.team.tableCode} + { + if (answer) { + await callServerAction(deleteTeamJudging, { + teamJudgingId: teamJudging.team + ?.teamJudgingId as number, + }); + } + }} + > + + + + ) : ( + ({ + value: team.teamId.toString(), + label: team.nameAndTable, + }))} + /> + )} +
+ ))}
); }; diff --git a/src/scenes/Dashboard/scenes/Judging/scenes/JudgingResults/JudgingResults.tsx b/src/scenes/Dashboard/scenes/Judging/scenes/JudgingResults/JudgingResults.tsx index 09a83d4..6a5cfdf 100644 --- a/src/scenes/Dashboard/scenes/Judging/scenes/JudgingResults/JudgingResults.tsx +++ b/src/scenes/Dashboard/scenes/Judging/scenes/JudgingResults/JudgingResults.tsx @@ -1,4 +1,7 @@ import React from "react"; +import Link from "next/link"; +import { Stack } from "@/components/ui/stack"; +import { ChevronLeftIcon } from "@heroicons/react/24/outline"; import getJudgingResults from "@/server/getters/dashboard/judging/getJudgingResults"; type JudgingResultsProps = { @@ -8,12 +11,21 @@ const JudgingResults = async ({ hackathonId }: JudgingResultsProps) => { const results = await getJudgingResults(hackathonId); return (
+ + + + Back to judging + + Results:{" "}
{results.map((result, index) => { return (
- {index + 1}. {result.name} - {result.score} + {index + 1}. {result.name} ({result.tableCode}) - {result.score}
); })} diff --git a/src/server/actions/dashboard/judging/createTeamJudging.ts b/src/server/actions/dashboard/judging/createTeamJudging.ts index 622371b..c06be77 100644 --- a/src/server/actions/dashboard/judging/createTeamJudging.ts +++ b/src/server/actions/dashboard/judging/createTeamJudging.ts @@ -3,6 +3,7 @@ import { prisma } from "@/services/prisma"; import requireAdminSession from "@/server/services/helpers/auth/requireAdminSession"; import { revalidatePath } from "next/cache"; +import { ExpectedServerActionError } from "@/services/types/serverErrors"; type CreateTeamJudgingInput = { organizerId: number; @@ -27,6 +28,22 @@ const createTeamJudging = async ({ throw new Error("Judging slot not found"); } + const existingTeamJudging = await prisma.teamJudging.findFirst({ + where: { + judgingSlotId, + teamId, + }, + select: { + id: true, + }, + }); + + if (existingTeamJudging) { + throw new ExpectedServerActionError( + "Team already assigned to this judging slot" + ); + } + await prisma.teamJudging.create({ data: { judgingSlotId, diff --git a/src/server/getters/dashboard/judging/getJudgingResults.ts b/src/server/getters/dashboard/judging/getJudgingResults.ts index cfe2758..cb7abb9 100644 --- a/src/server/getters/dashboard/judging/getJudgingResults.ts +++ b/src/server/getters/dashboard/judging/getJudgingResults.ts @@ -4,6 +4,7 @@ import { prisma } from "@/services/prisma"; export type TeamForResult = { id: number; name: string; + tableCode: string; score: number; }; @@ -12,17 +13,124 @@ type ParsedVerdict = Record; type TeamsWithJudgings = { teamId: number; teamName: string; - judgingVerdicts: ParsedVerdict[]; + tableCode: string; + judgingVerdicts: { + judgingSlotStartTime: Date; + organizerId: number; + verdict: ParsedVerdict; + }[]; }[]; const computeJudgingResults = ( teamsWithJudgings: TeamsWithJudgings ): TeamForResult[] => { - // TODO: Implement this function + // Define the time equal to 12:50:00 + // Hack Kosice 2024 specific time, used to split the teams into two groups + // The comparisons will only work if the judging slots are in the same day, + // that is they need to be created on the day of the judging + const timeThreshold = new Date(); + timeThreshold.setHours(12); + timeThreshold.setMinutes(50); + timeThreshold.setSeconds(0); + timeThreshold.setMilliseconds(0); + + // Filter the teams with the judgingSlotStartTimes lower that timeThreshold + // if one time is lower than the threshold, we assume all are and the team + // is placed in the first group + const firtsGroup = teamsWithJudgings.filter( + (team) => + (team.judgingVerdicts[0] == undefined + ? Infinity // Teams without any judgings are discarded + : team.judgingVerdicts[0].judgingSlotStartTime.getTime()) < + timeThreshold.getTime() + ); + + // The rest is considered the second group + const secondGroup = teamsWithJudgings.filter( + (team) => + (team.judgingVerdicts[0] == undefined + ? 0 // Teams without any judgings are discarded + : team.judgingVerdicts[0].judgingSlotStartTime.getTime()) >= + timeThreshold.getTime() + ); + + return processOneGroup(firtsGroup).concat(processOneGroup(secondGroup)); +}; + +const processOneGroup = ( + teamsWithJudgings: TeamsWithJudgings +): TeamForResult[] => { + // Define a dictionary of teams and their final scores + const teamScores: Record = {}; + + // Create a set of all organizer ids + const organizerIds = new Set( + teamsWithJudgings.flatMap((team) => + team.judgingVerdicts.map((judging) => judging.organizerId) + ) + ); + + organizerIds.forEach((organizerId) => { + // Calculate the number of verdicts made by this organizer + const numberOfVerdicts = teamsWithJudgings.reduce((acc, team) => { + return ( + acc + + team.judgingVerdicts.filter( + (judging) => judging.organizerId === organizerId + ).length + ); + }, 0); + + // Count the total number of points awarded by the organizer in the verdicts + const totalPoints = teamsWithJudgings.reduce((acc, team) => { + return ( + acc + + team.judgingVerdicts + .filter((judging) => judging.organizerId === organizerId) + .reduce((acc, judging) => { + return ( + acc + + Object.values(judging.verdict).reduce( + (acc, value) => acc + value, + 0 + ) + ); + }, 0) + ); + }, 0); + + // For each team, calculate the proportion of the score given by the particular organizer + // multiplied by the number of their verdicts and add it to the team's score + teamsWithJudgings.forEach((team) => { + const points = team.judgingVerdicts + .filter((judging) => judging.organizerId === organizerId) + .reduce((acc, judging) => { + return ( + acc + + Object.values(judging.verdict).reduce( + (acc, value) => acc + value, + 0 + ) + ); + }, 0); + + if (points === 0) { + return; + } + + if (!teamScores[team.teamId]) { + teamScores[team.teamId] = 0; + } + + teamScores[team.teamId] += (points / totalPoints) * numberOfVerdicts; + }); + }); + return teamsWithJudgings.map((team) => ({ id: team.teamId, name: team.teamName, - score: 0, + tableCode: team.tableCode, + score: (teamScores[team.teamId] || 0) / team.judgingVerdicts.length, })); }; @@ -43,13 +151,24 @@ const getJudgingResults = async ( const { fullyConfirmedTeams } = await getConfirmedTeams(hackathonId); const teamJudgings = await prisma.teamJudging.findMany({ select: { + judgingSlot: { + select: { + startTime: true, + }, + }, judgingVerdict: true, team: { select: { id: true, name: true, + table: { + select: { + code: true, + }, + }, }, }, + organizerId: true, }, }); @@ -63,13 +182,24 @@ const getJudgingResults = async ( teamsWithJudgings.push({ teamId: team.id, teamName: team.name, + tableCode: team.tableCode || "", judgingVerdicts: judgings - .map((judging) => - judging.judgingVerdict - ? parseJudgingVerdict(judging.judgingVerdict) - : null - ) - .filter((judging) => judging !== null) as ParsedVerdict[], + .map((judging) => { + if (!judging.judgingVerdict) { + return null; + } + const parsedVerdict = parseJudgingVerdict(judging.judgingVerdict); + return { + judgingSlotStartTime: judging.judgingSlot.startTime, + organizerId: judging.organizerId, + verdict: parsedVerdict, + }; + }) + .filter((judging) => judging !== null) as { + judgingSlotStartTime: Date; + organizerId: number; + verdict: ParsedVerdict; + }[], }); }