Files
NextEdu/src/modules/homework/components/student-homework-review-view.tsx
SpecialX 4f0ef217a0 refactor(modules): update existing module implementations across attendance, audit, auth, classes, course-plans, exams, files, homework, layout, proctoring, questions, scheduling, textbooks, users
- Update attendance components and data-access for record management

- Update audit log views, filters, and data-access

- Update auth login and register forms

- Update classes actions, components, and data-access (admin, schedule, stats)

- Update course-plans actions, form, list, progress, and schema

- Update exams actions, AI pipeline, preview components, and hooks

- Update files components (icon, list, preview, upload) and data-access

- Update homework assignment form, review view, auto-save hook, and stats-service

- Update layout sidebar, header, and navigation config

- Update proctoring actions, anti-cheat monitor, and data-access

- Update questions actions, components (dialog, actions, columns, filters), and data-access

- Update scheduling actions, auto-scheduler, components, and schema

- Update textbooks constants and text-selection hook

- Update users class-registration, import-dialog, data-access, and user-service
2026-06-23 17:38:56 +08:00

223 lines
9.7 KiB
TypeScript

"use client"
import { useMemo } from "react"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
import { Label } from "@/shared/components/ui/label"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import { FileText, ChevronLeft } from "lucide-react"
import Link from "next/link"
import { useTranslations } from "next-intl"
import type { StudentHomeworkTakeData } from "../types"
import { QuestionRenderer } from "./question-renderer"
import {
getCorrectnessState,
parseSavedAnswer,
} from "../lib/question-content-utils"
type HomeworkReviewViewProps = {
initialData: StudentHomeworkTakeData
}
export function HomeworkReviewView({ initialData }: HomeworkReviewViewProps) {
const t = useTranslations("examHomework")
const submissionStatus = initialData.submission?.status ?? "not_started"
const isGraded = submissionStatus === "graded"
const answersByQuestionId = useMemo(() => {
const map = new Map<string, { answer: unknown }>()
for (const q of initialData.questions) {
map.set(q.questionId, parseSavedAnswer(q.savedAnswer, q.questionType))
}
const obj: Record<string, { answer: unknown }> = {}
for (const [k, v] of map.entries()) obj[k] = v
return obj
}, [initialData.questions])
return (
<div className="grid h-[calc(100vh-10rem)] grid-cols-1 gap-6 lg:grid-cols-12">
<div className="lg:col-span-9 flex flex-col h-full overflow-hidden rounded-md border bg-card">
<div className="border-b p-4 flex items-center justify-between bg-muted/30">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<FileText className="h-4 w-4 text-primary" />
</div>
<div>
<h3 className="font-semibold leading-none">
{isGraded ? t("homework.review.gradedReport") : t("homework.review.submissionDetails")}
</h3>
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] capitalize">
{submissionStatus}
</Badge>
<span></span>
<span>{initialData.questions.length} {t("homework.review.questionsUnit")}</span>
</div>
</div>
</div>
<Button asChild variant="outline" size="sm">
<Link href="/student/learning/assignments">
<ChevronLeft className="mr-2 h-4 w-4" />
{t("homework.review.backToList")}
</Link>
</Button>
</div>
<ScrollArea className="flex-1 bg-muted/10">
<div className="space-y-6 p-6 max-w-4xl mx-auto">
{initialData.questions.map((q, idx) => {
const value = answersByQuestionId[q.questionId]?.answer
const correctness = isGraded
? getCorrectnessState({ score: q.score ?? null, maxScore: q.maxScore })
: "ungraded"
const borderClass =
correctness === "correct"
? "border-l-4 border-l-emerald-500"
: correctness === "incorrect"
? "border-l-4 border-l-red-500"
: correctness === "partial"
? "border-l-4 border-l-yellow-500"
: "border-l-4 border-l-primary"
return (
<Card key={q.questionId} className={`shadow-sm ${borderClass}`}>
<CardHeader className="pb-2">
<QuestionRenderer
questionId={q.questionId}
questionType={q.questionType}
questionContent={q.questionContent}
maxScore={q.maxScore}
index={idx}
mode="review"
value={value}
showCorrectAnswer={isGraded}
feedback={isGraded ? q.feedback : null}
headerExtra={
isGraded ? (
<Badge
variant="outline"
aria-label={t(`homework.grade.${correctness === "ungraded" ? "partial" : correctness}`)}
className={
correctness === "correct"
? "text-emerald-600 border-emerald-200 bg-emerald-50"
: "text-red-600 border-red-200 bg-red-50"
}
>
{q.score} / {q.maxScore}
</Badge>
) : null
}
/>
</CardHeader>
<CardContent className="space-y-4 pt-0">
{q.knowledgePoints && q.knowledgePoints.length > 0 && (
<div className="flex flex-wrap gap-1">
{q.knowledgePoints.map((kp) => (
<Badge key={kp.id} variant="secondary" className="text-[10px] px-1.5 py-0 h-5">
{kp.name}
</Badge>
))}
</div>
)}
</CardContent>
</Card>
)
})}
</div>
</ScrollArea>
</div>
<div className="lg:col-span-3 flex flex-col h-full overflow-hidden rounded-md border bg-card">
<div className="border-b p-4 bg-muted/30">
<h3 className="font-semibold">{t("homework.take.assignmentInfo")}</h3>
</div>
<div className="flex-1 p-4 overflow-y-auto">
<div className="space-y-6">
<div>
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t("homework.take.status")}</Label>
<div className="mt-1 flex items-center gap-2">
<Badge variant="secondary" className="capitalize">
{submissionStatus}
</Badge>
</div>
</div>
<div>
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t("homework.review.description")}</Label>
<p className="mt-1 text-sm text-muted-foreground leading-relaxed">
{initialData.assignment.description || t("homework.review.noDescription")}
</p>
</div>
{isGraded && (
<div>
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t("homework.review.totalScore")}</Label>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold text-primary">
{initialData.submission?.score ?? 0}
</span>
<span className="text-sm text-muted-foreground">
/ {initialData.questions.reduce((acc, q) => acc + q.maxScore, 0)}
</span>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-3 w-3 rounded-full bg-emerald-600" aria-hidden="true" />
<span>{t("homework.grade.correct")}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-3 w-3 rounded-full bg-yellow-500" aria-hidden="true" />
<span>{t("homework.grade.partial")}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-3 w-3 rounded-full bg-red-500" aria-hidden="true" />
<span>{t("homework.grade.incorrect")}</span>
</div>
</div>
</div>
)}
<div>
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
{isGraded ? t("homework.review.questionBreakdown") : t("homework.review.responseSummary")}
</Label>
<div className="mt-2 grid grid-cols-5 gap-2">
{initialData.questions.map((q, i) => {
const answer = answersByQuestionId[q.questionId]?.answer
const hasAnswer = answer !== undefined &&
answer !== "" &&
(Array.isArray(answer) ? answer.length > 0 : true)
const score = q.score ?? 0
const max = q.maxScore
let statusClass = "bg-background text-muted-foreground border-input"
if (isGraded) {
if (score === max && max > 0) statusClass = "bg-emerald-600 text-white border-emerald-600"
else if (score > 0) statusClass = "bg-yellow-500 text-white border-yellow-500"
else statusClass = "bg-red-500 text-white border-red-500"
} else if (hasAnswer) {
statusClass = "bg-primary text-primary-foreground border-primary"
}
return (
<div
key={q.questionId}
className={`h-8 w-8 rounded flex items-center justify-center text-xs font-medium border ${statusClass}`}
>
{i + 1}
</div>
)
})}
</div>
</div>
</div>
</div>
</div>
</div>
)
}