// Path: Frontend/src/components/assignment/teacher/assignment-teacher-review.tsx import type { Component } from "solid-js"; import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; import { useNavigate } from "@solidjs/router"; import type { TeacherAssignmentReviewPageData, TeacherReviewDraftFields, TeacherReviewNotice, TeacherReviewQuestion } from "./assignment-teacher-review.types"; import { closeTeacherAssignment, updateTeacherAnswerReview } from "./assignment-teacher-review.data"; import { applyReviewDrafts, type AssignmentReviewDraftStore, countPendingQuestionDrafts, EMPTY_DRAFT_STORE, type StudentReviewDraft, readDraftStore, reviewFieldsEqual, reviewFieldsFromQuestion, sanitizeReviewFields, writeDraftStore, } from "./assignment-teacher-review.drafts"; import styles from "./assignment-teacher-review.module.scss"; import { TeacherReviewQuestionCard, TeacherReviewSaveProgressCard, TeacherReviewSidebar } from "./assignment-teacher-review.sections"; import { getTeacherAssignmentNextStepHref } from "../../../lib/routes"; type Props = { data: TeacherAssignmentReviewPageData; onSelectStudent: (studentId: number) => void; onRefresh: () => Promise | unknown; }; const AssignmentTeacherReview: Component = (props) => { const navigate = useNavigate(); let questionReviewSectionRef: HTMLElement | undefined; const [draftStore, setDraftStore] = createSignal(EMPTY_DRAFT_STORE); const [savingAll, setSavingAll] = createSignal(false); const [closingAssignment, setClosingAssignment] = createSignal(false); const [notice, setNotice] = createSignal(null); const [closeNotice, setCloseNotice] = createSignal(null); const [questionNavExpanded, setQuestionNavExpanded] = createSignal(false); const currentStudentKey = createMemo(() => (props.data.selectedStudentId ? String(props.data.selectedStudentId) : null)); const currentStudentDraft = createMemo(() => { const studentKey = currentStudentKey(); if (!studentKey) return null; return draftStore().students[studentKey] ?? null; }); const displayQuestions = createMemo(() => applyReviewDrafts(props.data.questions, currentStudentDraft())); const pendingChangeCount = createMemo(() => countPendingQuestionDrafts(draftStore())); createEffect(() => { setDraftStore(readDraftStore(props.data.assignmentId)); }); createEffect(() => { writeDraftStore(props.data.assignmentId, draftStore()); }); createEffect(() => { setNotice(null); }); createEffect(() => { setCloseNotice(null); }); const hasQueue = createMemo(() => props.data.reviewQueue.length > 0); const canMoveToNextStep = createMemo(() => { return props.data.selectedStudentSubmittedQuestions > 0 || props.data.selectedStudentReviewedQuestions > 0; }); const getBaseQuestion = (questionId: number) => props.data.questions.find((entry) => entry.id === questionId); const getSelectedStudentQuestions = () => props.data.questions.filter((question) => question.answerId); const getQuestionCardId = (questionId: number) => `teacher-review-question-${questionId}`; const handleSelectQuestion = (questionId: number) => { const element = document.getElementById(getQuestionCardId(questionId)); element?.scrollIntoView({ behavior: "smooth", block: "start" }); setQuestionNavExpanded(false); }; const handleScrollToTop = () => { questionReviewSectionRef?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const updateStudentDraft = (studentId: number, updater: (draft: StudentReviewDraft) => StudentReviewDraft | null) => { const studentKey = String(studentId); setDraftStore((current) => { const nextStudents = { ...current.students }; const nextDraft = updater(nextStudents[studentKey] ?? {}); if (!nextDraft) { delete nextStudents[studentKey]; } else { nextStudents[studentKey] = nextDraft; } return { students: nextStudents, }; }); }; const updateQuestionReviewDraft = (question: TeacherReviewQuestion, patch: Partial) => { if (!question.answerId || !props.data.selectedStudentId) return; setNotice(null); const baseQuestion = getBaseQuestion(question.id) ?? question; const baseFields = sanitizeReviewFields(reviewFieldsFromQuestion(baseQuestion)); updateStudentDraft(props.data.selectedStudentId, (draft) => { const questionReviews = { ...(draft.questionReviews ?? {}) }; const currentFields = sanitizeReviewFields( questionReviews[String(question.id)] ? { ...baseFields, ...questionReviews[String(question.id)], } : baseFields ); const nextFields = sanitizeReviewFields({ ...currentFields, ...patch, }); if (reviewFieldsEqual(nextFields, baseFields)) { delete questionReviews[String(question.id)]; } else { questionReviews[String(question.id)] = { answerId: question.answerId, ...nextFields, }; } const nextDraft: StudentReviewDraft = { ...draft, }; if (Object.keys(questionReviews).length > 0) { nextDraft.questionReviews = questionReviews; } else { delete nextDraft.questionReviews; } if (Object.keys(nextDraft.questionReviews ?? {}).length === 0) { return null; } return nextDraft; }); }; const resetQuestionDraft = (question: TeacherReviewQuestion) => { const selectedStudentId = props.data.selectedStudentId; if (!selectedStudentId) return; setNotice(null); updateStudentDraft(selectedStudentId, (draft) => { const questionReviews = { ...(draft.questionReviews ?? {}) }; delete questionReviews[String(question.id)]; if (Object.keys(questionReviews).length === 0) { return null; } return { ...draft, questionReviews, }; }); }; const markQuestionNeedsAttention = (question: TeacherReviewQuestion) => { updateQuestionReviewDraft(question, { reviewNeedsAttention: true, }); }; const saveAllAndNextStep = async () => { setNotice(null); const selectedStudentId = props.data.selectedStudentId; if (!selectedStudentId) return; if (!canMoveToNextStep()) { setNotice({ scope: "global", tone: "error", text: "Students need a submitted review before you can move them to the next step.", }); return; } const operations: Array<() => Promise> = []; const selectedStudentDraft = draftStore().students[String(selectedStudentId)] ?? null; const questionDrafts = selectedStudentDraft?.questionReviews ?? {}; for (const question of getSelectedStudentQuestions()) { const draft = questionDrafts[String(question.id)]; if (!question.answerId) continue; const baseFields = sanitizeReviewFields(reviewFieldsFromQuestion(question)); const effectiveReview = sanitizeReviewFields( draft ? { ...baseFields, ...draft, } : baseFields ); const nextStatus = effectiveReview.reviewNeedsAttention ? "submitted" : "reviewed"; const hasReviewFieldChanges = !reviewFieldsEqual(effectiveReview, baseFields); if (question.answerStatus === nextStatus && !hasReviewFieldChanges) continue; operations.push(() => updateTeacherAnswerReview(question.answerId!, { status: nextStatus, reviewTags: [...question.questionTags], ...effectiveReview, }) ); } setSavingAll(true); try { for (const operation of operations) { await operation(); } updateStudentDraft(selectedStudentId, () => null); navigate(getTeacherAssignmentNextStepHref(props.data.assignmentId, selectedStudentId)); } catch (error) { setNotice({ scope: "global", tone: "error", text: error instanceof Error ? error.message : "Could not save all review changes right now.", }); } finally { setSavingAll(false); } }; const handleCloseAssignment = async () => { setCloseNotice(null); if (!props.data.closeSummary.canClose) { setCloseNotice({ scope: "assignment", tone: "error", text: props.data.closeSummary.summary, }); return; } setClosingAssignment(true); try { await closeTeacherAssignment(props.data.assignmentId); await props.onRefresh(); setCloseNotice({ scope: "assignment", tone: "success", text: "Assignment closed.", }); } catch (error) { setCloseNotice({ scope: "assignment", tone: "error", text: error instanceof Error ? error.message : "Could not close this assignment right now.", }); } finally { setClosingAssignment(false); } }; return ( <>

Question review

{props.data.selectedStudentName} ยท {props.data.questions.length} questions

0}>

Questions

{displayQuestions().length} questions in this review

{(question) => ( )}
Select a student from the review queue to inspect answers and leave feedback.
}>
{(question) => ( )}
); }; export default AssignmentTeacherReview;