Skip to content

Commit

Permalink
Judging results calculation (#62)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Michal Masrna <[email protected]>
  • Loading branch information
3 people authored Apr 7, 2024
1 parent 63a4f3a commit 478f5ea
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 51 deletions.
4 changes: 2 additions & 2 deletions src/scenes/Dashboard/scenes/Judging/Judging.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Judging = async ({ hackathonId }: JudgingManagerProps) => {
</CardHeader>
<CardContent>
{session?.isAdmin && (
<>
<div className="flex flex-row gap-1">
<Button>
<Link href={`/dashboard/${hackathonId}/judging/manage`}>
Judging manager
Expand All @@ -31,7 +31,7 @@ const Judging = async ({ hackathonId }: JudgingManagerProps) => {
Judging results
</Link>
</Button>
</>
</div>
)}
<JudgingSwitcher
judgings={judgings}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,55 @@ const JudgingManagerJudgeTimesheet = ({
}: JudgingManagerJudgeTimesheetProps) => {
return (
<div className="mt-5">
{judge.teamJudgings.map((teamJudging) => (
<div
key={teamJudging.judgingSlot.id}
className="flex flex-row items-center gap-1"
>
<span>
{dateToTimeString(teamJudging.judgingSlot.startTime)} -{" "}
{dateToTimeString(teamJudging.judgingSlot.endTime)}
</span>
{teamJudging.team ? (
<>
<span>{teamJudging.team.name}</span>
<ConfirmationDialog
question="Are you sure you want to unassign this team?"
onAnswer={async (answer) => {
if (answer) {
await callServerAction(deleteTeamJudging, {
teamJudgingId: teamJudging.team?.teamJudgingId as number,
});
}
}}
>
<Button variant="ghost" className="text-red-500 p-0">
<X className="h-4 w-4" />
</Button>
</ConfirmationDialog>
</>
) : (
<NewTeamJudgingDialog
judgingSlotId={teamJudging.judgingSlot.id}
organizerId={judge.id}
teamOptions={teamsForJudging.map((team) => ({
value: team.teamId.toString(),
label: team.nameAndTable,
}))}
/>
)}
</div>
))}
{judge.teamJudgings
.sort((teamJudgingA, teamJudgingB) => {
return (
teamJudgingA.judgingSlot.startTime.getTime() -
teamJudgingB.judgingSlot.startTime.getTime()
);
})
.map((teamJudging) => (
<div
key={teamJudging.judgingSlot.id}
className="flex flex-row items-center gap-1"
>
<span>
{dateToTimeString(teamJudging.judgingSlot.startTime)} -{" "}
{dateToTimeString(teamJudging.judgingSlot.endTime)}
</span>
{teamJudging.team ? (
<>
<span>{teamJudging.team.name}</span>
{" - "}
<span>{teamJudging.team.tableCode}</span>
<ConfirmationDialog
question="Are you sure you want to unassign this team?"
onAnswer={async (answer) => {
if (answer) {
await callServerAction(deleteTeamJudging, {
teamJudgingId: teamJudging.team
?.teamJudgingId as number,
});
}
}}
>
<Button variant="ghost" className="text-red-500 p-0">
<X className="h-4 w-4" />
</Button>
</ConfirmationDialog>
</>
) : (
<NewTeamJudgingDialog
judgingSlotId={teamJudging.judgingSlot.id}
organizerId={judge.id}
teamOptions={teamsForJudging.map((team) => ({
value: team.teamId.toString(),
label: team.nameAndTable,
}))}
/>
)}
</div>
))}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -8,12 +11,21 @@ const JudgingResults = async ({ hackathonId }: JudgingResultsProps) => {
const results = await getJudgingResults(hackathonId);
return (
<div>
<Link
href={`/dashboard/${hackathonId}/judging`}
className="text-hkOrange"
>
<Stack direction="row" alignItems="center" spacing="small">
<ChevronLeftIcon className="h-5 w-5" />
Back to judging
</Stack>
</Link>
Results:{" "}
<div>
{results.map((result, index) => {
return (
<div key={index}>
{index + 1}. {result.name} - {result.score}
{index + 1}. {result.name} ({result.tableCode}) - {result.score}
</div>
);
})}
Expand Down
17 changes: 17 additions & 0 deletions src/server/actions/dashboard/judging/createTeamJudging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
148 changes: 139 additions & 9 deletions src/server/getters/dashboard/judging/getJudgingResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { prisma } from "@/services/prisma";
export type TeamForResult = {
id: number;
name: string;
tableCode: string;
score: number;
};

Expand All @@ -12,17 +13,124 @@ type ParsedVerdict = Record<string, number>;
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<number, number> = {};

// 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,
}));
};

Expand All @@ -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,
},
});

Expand All @@ -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;
}[],
});
}

Expand Down

0 comments on commit 478f5ea

Please sign in to comment.