From 0236495a940f51ec22a5cfd7ea40afd6d41e34f5 Mon Sep 17 00:00:00 2001 From: Owen Gretzinger Date: Fri, 10 Jan 2025 17:05:13 -0500 Subject: [PATCH] feat: improve judging form design, show table projects list (#236) * slight improvements * refactor redundant code * add projects list * improve form design * fix small bug where no project is auto selected when all projects have already been judged * fix: show no project left message when judging is done --------- Co-authored-by: Arian Ahmadinejad --- src/pages/judging.tsx | 542 +++++++++++++++++++++-------------- src/server/router/judging.ts | 115 ++++++-- 2 files changed, 417 insertions(+), 240 deletions(-) diff --git a/src/pages/judging.tsx b/src/pages/judging.tsx index 175001f7..8d4fc3eb 100644 --- a/src/pages/judging.tsx +++ b/src/pages/judging.tsx @@ -25,22 +25,40 @@ type TableOption = z.infer; // Add this type for better type safety type ScoreType = 0 | 1 | 2 | 3; +const formatTime = (date: Date) => { + return new Date(date).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); +}; + const Judging: NextPage = () => { const { control, handleSubmit, reset, register } = useForm(); // const [currentProject, setCurrentProject] = useState(null); const [selectedTable, setSelectedTable] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useState( + null + ); const { data: tables, isLoading: tablesLoading } = trpc.table.getTables.useQuery(); const { mutate: submitJudgment } = trpc.judging.createJudgingResult.useMutation(); + // TODO: Change this endpoint to only handle one thing, not filtering on projectId const { data: nextProject, refetch: refetchNextProject, isSuccess: projectSuccess, + isLoading: isProjectLoading, } = trpc.project.getNextProject.useQuery( - { tableId: selectedTable?.value || "" }, - { enabled: !!selectedTable } + { + tableId: selectedTable?.value || "", + projectId: selectedProjectId, + }, + { + enabled: !!selectedTable, + keepPreviousData: true, + } ); const generalTrackId = tables?.find( (t) => t.track.name.toLowerCase() === "general" @@ -76,6 +94,32 @@ const Judging: NextPage = () => { : []) || []), ]; + const { data: tableProjects, refetch: refetchTableProjects } = + trpc.table.getTableProjects.useQuery( + { tableId: selectedTable?.value || "" }, + { enabled: !!selectedTable } + ); + + const { data: existingScores, refetch: refetchExistingScores } = + trpc.judging.getProjectScores.useQuery( + { projectId: nextProject?.id || "" }, + { enabled: !!nextProject } + ); + + useEffect(() => { + if (existingScores && nextProject) { + // Reset form with existing scores + const scores: Record = {}; + existingScores.forEach((response) => { + scores[response.questionId] = response.score; + }); + reset({ scores }); + } else { + // Reset form when switching to a new project + reset({ scores: {} }); + } + }, [existingScores, nextProject, reset]); + const onSubmit = (data: any) => { if (!nextProject?.id) return; @@ -91,8 +135,19 @@ const Judging: NextPage = () => { }, { onSuccess: () => { - reset(); + // Only clear selectedProjectId if there are more unjudged projects + const hasMoreUnjudgedProjects = tableProjects?.some( + (p) => !p.isJudged && p.id !== nextProject.id + ); + if (hasMoreUnjudgedProjects) { + setSelectedProjectId(null); + } + refetchExistingScores(); refetchNextProject(); + refetchTableProjects(); + reset({ scores: {} }); + // Scroll to top of page + window.scrollTo({ top: 0, behavior: "smooth" }); }, } ); @@ -104,245 +159,286 @@ const Judging: NextPage = () => { label: `Table ${table.number} - ${table.track.name}`, })) || []; + const QuestionSection = ({ + title, + questions, + control, + }: { + title: string; + questions: typeof rubricQuestions; + control: any; + }) => ( +
+

{title}

+
+ {questions?.map((question) => ( +
+
+ {/* Question header with points */} +
+

{question.title}

+
+ {question.points} points +
+
+ + {/* Main question text */} +
+ {question.question} +
+ + {/* Scoring section */} +
+
+ Score: +
+ ( +
+
+ {[0, 1, 2, 3].map((score) => ( + + ))} +
+
+ {value === 0 && "0 - Ineffective / Bad"} + {value === 1 && "1 - Limited / Okay"} + {value === 2 && "2 - Functional / Good"} + {value === 3 && "3 - Exceptional / Phenomenal"} +
+
+ )} + /> +
+
+
+ ))} +
+
+ ); + return ( <> Dashboard - DeltaHacks XI -
+
-
-
-

Project Judging

+
+
+
+

Project Judging

+ {/* Table selection */} +
+ + ( + { - field.onChange(option); - setSelectedTable(option as TableOption); - }} - unstyled={true} - classNames={{ - control: (state) => - state.menuIsOpen - ? "rounded-md p-3 dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700 bg-white border" - : "rounded-md p-3 dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700 bg-white border", - menu: () => - "dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700 bg-white border -mt-1 rounded-b-lg overflow-hidden", - option: () => - "p-2 dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-900", - valueContainer: () => - "dark:text-neutral-500 text-neutral-700 gap-2", - singleValue: () => "dark:text-white text-black", - }} - /> +
+

No projects available

+

+ There are no more projects available for judging at this + time. +

+
)} - /> -
- - {projectSuccess && nextProject && ( -
-
-

- {nextProject.name} -

-

{nextProject.description}

- - Project Link - - - {allQuestions && allQuestions.length > 0 ? ( -
- {/* Track-specific questions section */} - {rubricQuestions && - rubricQuestions.length > 0 && - tables - ?.find((t) => t.id === selectedTable?.value) - ?.track.name.toLowerCase() !== "general" && ( -
-

- { - tables?.find( - (t) => t.id === selectedTable?.value - )?.track.name - }{" "} - Track Questions -

- {rubricQuestions.map((question, index) => ( -
- {index > 0 && ( -
- )} -
- -
- ( - <> -
- {[0, 1, 2, 3].map((score) => ( - - ))} -
-
- {value === 0 && - "0 - Ineffective / Bad"} - {value === 1 && - "1 - Limited / Okay"} - {value === 2 && - "2 - Functional / Good"} - {value === 3 && - "3 - Exceptional / Phenomenal"} -
- - )} - /> -
-
-
- ))} + {nextProject && selectedTable && ( +
+
+
+
+
+
+ Project Name +
+

+ {nextProject.name} +

+
+ - )} +
+
+ Project Description +
+

+ {nextProject.description || + "No description available"} +

+
+
+
- {/* General questions section */} -
-

- {tables + {/* Rest of the form */} + {allQuestions && allQuestions.length > 0 ? ( + + {/* Track-specific questions */} + {rubricQuestions && + rubricQuestions.length > 0 && + tables + ?.find((t) => t.id === selectedTable?.value) + ?.track.name.toLowerCase() !== "general" && ( + t.id === selectedTable?.value + )?.track.name + } Track Questions`} + questions={rubricQuestions} + control={control} + /> + )} + + {/* General questions */} + {(tables ?.find((t) => t.id === selectedTable?.value) ?.track.name.toLowerCase() === "general" - ? "General Track Questions" - : "General Questions"} -

- {(tables - ?.find((t) => t.id === selectedTable?.value) - ?.track.name.toLowerCase() === "general" - ? rubricQuestions - : generalQuestions - )?.map((question, index) => ( -
- {index > 0 && ( -
- )} -
- -
- ( - <> -
- {[0, 1, 2, 3].map((score) => ( - - ))} -
-
- {value === 0 && - "0 - Ineffective / Bad"} - {value === 1 && "1 - Limited / Okay"} - {value === 2 && - "2 - Functional / Good"} - {value === 3 && - "3 - Exceptional / Phenomenal"} -
- - )} - /> + ? rubricQuestions + : generalQuestions) && ( + t.id === selectedTable?.value) + ?.track.name.toLowerCase() === "general" + ? "General Track Questions" + : "General Questions" + } + questions={ + tables + ?.find((t) => t.id === selectedTable?.value) + ?.track.name.toLowerCase() === "general" + ? rubricQuestions + : generalQuestions + } + control={control} + /> + )} + + + + ) : ( +
+ No rubric questions available for this track. +
+ )} +
+
+ )} + + {/* Projects list */} + {selectedTable && ( +
+ {tableProjects?.map((project, index) => ( +
- -
- - ) : ( -
- No rubric questions available for this track. -
- )} -
+ ))} +
+ )}
- )} - {!nextProject &&

No project left😔

} +
diff --git a/src/server/router/judging.ts b/src/server/router/judging.ts index 505a6663..5afc1a95 100644 --- a/src/server/router/judging.ts +++ b/src/server/router/judging.ts @@ -156,7 +156,12 @@ export const projectRouter = router({ return { message: "Tables created successfully" }; }), getNextProject: protectedProcedure - .input(z.object({ tableId: z.string() })) + .input( + z.object({ + tableId: z.string(), + projectId: z.string().nullable(), + }) + ) .query(async ({ ctx, input }) => { const dhYearConfig = await ctx.prisma.config.findUnique({ where: { name: "dhYear" }, @@ -169,6 +174,22 @@ export const projectRouter = router({ }); } + // If a specific project is requested, return it if it belongs to the table + if (input.projectId) { + const project = await ctx.prisma.project.findFirst({ + where: { + id: input.projectId, + TimeSlot: { + some: { + tableId: input.tableId, + }, + }, + }, + }); + if (project) return project; + } + + // Get already judged projects const judgedProjects = await ctx.prisma.judgingResult.findMany({ where: { judgeId: ctx.session.user.id, @@ -177,8 +198,7 @@ export const projectRouter = router({ select: { projectId: true }, }); - // Find projects in this track that haven't been judged - + // Find the next unjudged project by time slot order const timeSlot = await ctx.prisma.timeSlot.findFirst({ where: { tableId: input.tableId, @@ -195,6 +215,10 @@ export const projectRouter = router({ }, }); + if (!timeSlot) { + return null; + } + return timeSlot?.project; }), getAllProjects: protectedProcedure.query(async ({ ctx }) => { @@ -247,7 +271,25 @@ export const tableRouter = router({ getTableProjects: protectedProcedure .input(z.object({ tableId: z.string() })) .query(async ({ ctx, input }) => { - return ctx.prisma.project.findMany({ + const dhYearConfig = await ctx.prisma.config.findUnique({ + where: { name: "dhYear" }, + }); + + const judgedProjects = await ctx.prisma.judgingResult.findMany({ + where: { + judgeId: ctx.session.user.id, + dhYear: dhYearConfig?.value, + }, + select: { + projectId: true, + }, + }); + + const judgedProjectIds = new Set( + judgedProjects.map((jp) => jp.projectId) + ); + + const projects = await ctx.prisma.project.findMany({ where: { TimeSlot: { some: { @@ -260,12 +302,24 @@ export const tableRouter = router({ where: { tableId: input.tableId, }, + orderBy: { + startTime: "asc", + }, }, }, - orderBy: { - id: "asc", - }, }); + + // Sort projects by their first TimeSlot's startTime + const sortedProjects = projects.sort((a, b) => { + const aTime = a.TimeSlot[0]?.startTime.getTime() ?? 0; + const bTime = b.TimeSlot[0]?.startTime.getTime() ?? 0; + return aTime - bTime; + }); + + return sortedProjects.map((project) => ({ + ...project, + isJudged: judgedProjectIds.has(project.id), + })); }), }); @@ -322,16 +376,25 @@ export const judgingRouter = router({ }, }); - if (existingResult) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "You have already judged this project", - }); - } - - // Create the judging result and its responses in a transaction return ctx.prisma.$transaction(async (tx) => { - // Create the judging result + if (existingResult) { + // Update existing responses + await tx.rubricResponse.deleteMany({ + where: { judgingResultId: existingResult.id }, + }); + + await tx.rubricResponse.createMany({ + data: input.responses.map((response) => ({ + judgingResultId: existingResult.id, + questionId: response.questionId, + score: response.score, + })), + }); + + return existingResult; + } + + // Create new judgment if none exists const judgingResult = await tx.judgingResult.create({ data: { judgeId: ctx.session.user.id, @@ -340,7 +403,6 @@ export const judgingRouter = router({ }, }); - // Create all rubric responses await tx.rubricResponse.createMany({ data: input.responses.map((response) => ({ judgingResultId: judgingResult.id, @@ -352,6 +414,25 @@ export const judgingRouter = router({ return judgingResult; }); }), + + // Get existing scores + getProjectScores: protectedProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { + const result = await ctx.prisma.judgingResult.findUnique({ + where: { + judgeId_projectId: { + judgeId: ctx.session.user.id, + projectId: input.projectId, + }, + }, + include: { + responses: true, + }, + }); + + return result?.responses || []; + }), createRubricQuestion: protectedProcedure .input( z.object({