// Path: Frontend/src/components/assignment/student/assignment-review.data.ts import { apiFetchJson } from "../../../lib/api"; import type { ApiAssignment, ApiAssignmentStudentQuestionDetail, ApiClassroom, ApiListResponse, ApiUser } from "../../../lib/api-types"; import { getAssignmentWorkHref } from "../../../lib/routes"; import type { AssignmentPageData } from "../shared/assignment-types"; const formatDateLabel = (value: string | null) => { if (!value) return "No due date"; return new Intl.DateTimeFormat("en-GB", { weekday: "short", month: "short", day: "numeric", }).format(new Date(value)); }; const extractTopic = (assignment: ApiAssignment, questions: ApiAssignmentStudentQuestionDetail[]) => { const fromInstructions = assignment.instructions?.match(/^Topic:\s*(.+)$/i)?.[1]?.trim(); if (fromInstructions) return fromInstructions; return questions[0]?.subject ?? "Assignment"; }; const deriveStudentStatus = (questions: ApiAssignmentStudentQuestionDetail[], assignmentStatus: ApiAssignment["status"]) => { const total = questions.length; const answered = questions.filter((question) => question.answer_id).length; const reviewed = questions.filter((question) => question.answer_status === "reviewed").length; if (answered === 0) return "NOT_STARTED" as const; if (reviewed === total || assignmentStatus === "closed") return "SUBMITTED" as const; return "IN_PROGRESS" as const; }; const deriveStatusLabel = (status: AssignmentPageData["status"]) => { switch (status) { case "SUBMITTED": return "Reviewed"; case "IN_PROGRESS": return "In progress"; default: return "Not started"; } }; const deriveHeadline = (status: AssignmentPageData["status"], topic: string) => { switch (status) { case "SUBMITTED": return `Review how you did in ${topic}`; case "IN_PROGRESS": return "Keep going — you already have momentum here"; default: return "Start this assignment with a steady first pass"; } }; const deriveDescription = (status: AssignmentPageData["status"], answered: number, reviewed: number, total: number, topic: string) => { switch (status) { case "SUBMITTED": return `${reviewed} of ${total} questions have teacher-reviewed answers. Use this review view to see what landed well and what still needs another pass in ${topic}.`; case "IN_PROGRESS": return `You have touched ${answered} of ${total} questions so far. Finish the remaining questions while ${topic.toLowerCase()} still feels familiar.`; default: return `This assignment has ${total} questions. Start with a calm first pass, then come back here for one shared review summary across the assignment.`; } }; const derivePrimaryAction = (status: AssignmentPageData["status"]) => { switch (status) { case "SUBMITTED": return "Open workspace"; case "IN_PROGRESS": return "Continue assignment"; default: return "Start assignment"; } }; const mapQuestionStatus = (question: ApiAssignmentStudentQuestionDetail) => { if (question.is_correct === true) { return { statusLabel: "Correct", statusTone: "success" as const, }; } if (question.answer_status === "reviewed") { return { statusLabel: "Reviewed", statusTone: "success" as const, }; } if (question.answer_status === "submitted") { return { statusLabel: "Submitted", statusTone: "warning" as const, }; } if (question.answer_status === "in_progress") { return { statusLabel: "In progress", statusTone: "warning" as const, }; } return { statusLabel: "Not started", statusTone: "muted" as const, }; }; export const getAssignmentReviewPageData = async (assignmentId: number, studentId: number): Promise => { if (!Number.isFinite(assignmentId) || assignmentId <= 0) return null; try { const assignment = await apiFetchJson(`/api/assignments/${assignmentId}`); const [student, teacher, classrooms, questionDetails] = await Promise.all([ apiFetchJson(`/api/users/${studentId}`), apiFetchJson(`/api/users/${assignment.teacher_id}`), apiFetchJson>(`/api/teachers/${assignment.teacher_id}/classrooms`), apiFetchJson>(`/api/assignments/${assignmentId}/students/${studentId}/questions`), ]); const classroom = classrooms.data.find((entry) => entry.id === assignment.classroom_id); const questions = questionDetails.data; const topic = extractTopic(assignment, questions); const answeredCount = questions.filter((question) => question.answer_id).length; const reviewedCount = questions.filter((question) => question.answer_status === "reviewed").length; const assignmentAiFeedback = questions[0]?.assignment_ai_feedback?.trim() || ""; const assignmentTeacherFeedback = questions[0]?.assignment_teacher_feedback?.trim() || ""; const status = deriveStudentStatus(questions, assignment.status); const statusLabel = deriveStatusLabel(status); return { id: assignment.id, title: assignment.title, topic, status, statusLabel, dueLabel: formatDateLabel(assignment.due_at), studentName: student.full_name, classroomName: classroom?.name ?? "Classroom", tutorName: teacher.full_name, headline: deriveHeadline(status, topic), description: deriveDescription(status, answeredCount, reviewedCount, questions.length, topic), primaryAction: derivePrimaryAction(status), primaryHref: getAssignmentWorkHref(assignment.id), stats: [ { label: "Status", value: statusLabel }, { label: "Due", value: formatDateLabel(assignment.due_at) }, { label: "Questions", value: `${questions.length}` }, { label: status === "SUBMITTED" ? "Reviewed" : "Answered", value: `${status === "SUBMITTED" ? reviewedCount : answeredCount}/${questions.length}` }, ], coachCard: { title: "Review notes", description: `Use this review view to compare your latest answers with the shared AI and teacher feedback for ${topic.toLowerCase()}.`, items: [`${answeredCount} questions attempted`, `${reviewedCount} questions reviewed`, `${Math.max(questions.length - answeredCount, 0)} questions still untouched`], }, assignmentAiFeedback: assignmentAiFeedback || undefined, assignmentTeacherFeedback: assignmentTeacherFeedback || undefined, questions: questions.map((question) => { const questionStatus = mapQuestionStatus(question); return { id: question.question_id, order: question.position, prompt: question.prompt, topic: question.subject, subTopic: null, difficulty: "Backend", marks: null, statusLabel: questionStatus.statusLabel, statusTone: questionStatus.statusTone, responseLabel: question.answer_id ? "Latest answer" : "Status", responseValue: question.answer_text?.trim() || "No attempt yet", feedback: "", workingSteps: question.working_steps?.trim() || "", solveModeLabel: question.solve_mode === "step_by_step" ? "Step by step" : question.solve_mode === "solve_together" ? "Solve together" : question.solve_mode === "handwritten" ? "Handwritten" : question.solve_mode === "just_answer" ? "Just answer" : undefined, showAnswerKey: Boolean(question.correct_answer && question.answer_id), correctAnswer: question.correct_answer?.trim() || null, isCorrect: typeof question.is_correct === "boolean" ? question.is_correct : null, }; }), }; } catch (error) { if (error instanceof Error && error.message === "not_found") { return null; } throw error; } };