Skip to content

Commit a601468

Browse files
fix(teacher): exam submissions review was fully broken (#282) (#305)
The teacher exam-grading pages passed data shaped for an older component contract, so both pages were unusable: Submissions list (`.../submissions/page.tsx`): - mapped student_name / score / review_status, but ExamSubmissionsReview reads profiles.full_name / ai_score / final_score / status → every row showed "Unknown Student" with "undefined%" scores and all stat cards read 0. Submission detail (`.../submissions/[submissionId]/page.tsx`): - same field drift → "undefined%" scores and "Invalid Date". - filtered exam_questions / exam_answers / exam_question_scores / exam_scores by `tenant_id`, but none of those tables have a tenant_id column → every query errored → empty Questions list and no scores. (RLS isolates via the parent exam/submission, so the explicit filter was both wrong and unnecessary.) - never passed the `questions` / `answers` props the component reads, so even a successful query would have rendered nothing. - onSave expected `{ score, question_overrides[] }` but the component calls onSave(overrides) keyed by question id → the override never persisted. Fixes: align both pages to the component contract, drop the invalid tenant_id filters, pass questions/answers, and rewrite handleSave to consume the real overrides shape, recompute the overall score from per-question points, and persist to exam_question_scores / exam_scores / exam_submissions. Verified on Code Academy Pro (creator admin, alice's Python final): list shows Alice Student / AI Reviewed / 70%; detail renders all 10 questions with options and AI scores; overriding Q1 10→5 recomputes 70%→65%, marks the submission teacher_reviewed, and the list updates to Graded / 65%. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7b3a558 commit a601468

3 files changed

Lines changed: 93 additions & 40 deletions

File tree

app/[locale]/dashboard/teacher/courses/[courseId]/exams/[examId]/submissions/[submissionId]/page.tsx

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,21 @@ export default async function SubmissionDetailPage({ params }: { params: Promise
5151
)
5252
`)
5353
.eq('exam_id', parseInt(examId))
54-
.eq('tenant_id', tenantId)
54+
// exam_questions has no tenant_id column; isolation is enforced by RLS
55+
// via the parent exam (already validated above).
5556
.order('question_id', { ascending: true }),
5657
supabase
5758
.from('exam_answers')
5859
.select('*')
59-
.eq('submission_id', parseInt(submissionId))
60-
.eq('tenant_id', tenantId),
60+
.eq('submission_id', parseInt(submissionId)),
6161
supabase
6262
.from('exam_question_scores')
6363
.select('*')
64-
.eq('submission_id', parseInt(submissionId))
65-
.eq('tenant_id', tenantId),
64+
.eq('submission_id', parseInt(submissionId)),
6665
supabase
6766
.from('exam_scores')
6867
.select('*')
69-
.eq('submission_id', parseInt(submissionId))
70-
.eq('tenant_id', tenantId),
68+
.eq('submission_id', parseInt(submissionId)),
7169
])
7270

7371
// Build per-question data combining questions, answers, and AI scores
@@ -98,65 +96,106 @@ export default async function SubmissionDetailPage({ params }: { params: Promise
9896
}
9997
})
10098

99+
// Field names here must match what SubmissionReview reads on `submission`
100+
// (status, submitted_at, ai_score, final_score, teacher_feedback). They had
101+
// drifted (review_status / submission_date / score), so the header showed
102+
// "undefined%" scores and "Invalid Date".
103+
const aiScore = rawSubmission.score ?? null
104+
const finalScore = examScores?.[0]?.score ?? rawSubmission.score ?? null
101105
const submission = {
102106
submission_id: rawSubmission.submission_id,
103107
exam_id: rawSubmission.exam_id,
104108
student_id: rawSubmission.student_id,
105109
student_name: student?.full_name || t('manageCourse.studentList.unknownStudent'),
106-
submission_date: rawSubmission.submission_date,
107-
score: rawSubmission.score,
108-
review_status: rawSubmission.review_status || 'pending',
110+
status: rawSubmission.review_status || 'pending',
111+
submitted_at: rawSubmission.submission_date,
112+
ai_score: aiScore,
113+
final_score: finalScore,
114+
teacher_feedback: examScores?.[0]?.feedback || rawSubmission.feedback || (rawSubmission.ai_data as any)?.overall_feedback || '',
109115
ai_data: rawSubmission.ai_data,
110116
ai_model_used: rawSubmission.ai_model_used,
111-
overall_feedback: examScores?.[0]?.feedback || (rawSubmission.ai_data as any)?.overall_feedback || '',
112-
teacher_notes: examScores?.[0]?.teacher_notes || '',
113-
questions: questionData,
114117
}
115118

116-
const handleSave = async (overrides: {
117-
score: number
118-
feedback: string
119-
teacher_notes: string
120-
question_overrides: {
121-
question_id: number
122-
points_earned: number
123-
is_correct: boolean
124-
teacher_notes: string
125-
}[]
126-
}) => {
119+
// SubmissionReview reads `questions` and `answers` as their own props (not
120+
// nested under submission). It keys overrides by question id, so `id` must
121+
// equal the answer's question_id. exam_questions has no `sequence` column,
122+
// so fall back to question_id for ordering.
123+
const questionsForReview = questionData.map(q => ({
124+
id: q.question_id,
125+
sequence: q.question_id,
126+
question_type: q.question_type,
127+
question_text: q.question_text,
128+
points_possible: q.points_possible,
129+
answer_text: q.answer_text,
130+
options: (q.options || []).map((o: any) => ({
131+
id: o.option_id,
132+
option_text: o.option_text,
133+
is_correct: o.is_correct,
134+
})),
135+
}))
136+
137+
const answersForReview = questionData.map(q => ({
138+
question_id: q.question_id,
139+
points_earned: q.points_earned,
140+
is_correct: q.is_correct,
141+
teacher_notes: q.teacher_notes,
142+
teacher_score_override: q.is_overridden ? q.points_earned : null,
143+
}))
144+
145+
// SubmissionReview calls onSave(overrides) where `overrides` is keyed by
146+
// question id — NOT the { score, question_overrides[] } shape this previously
147+
// expected, so the override never persisted. Consume the real shape, recompute
148+
// the overall score from the per-question points, and persist.
149+
const handleSave = async (overrides: Record<number, {
150+
points_earned?: number
151+
is_correct?: boolean
152+
teacher_notes?: string
153+
is_overridden?: boolean
154+
}>) => {
127155
'use server'
128156
const supabase = createAdminClient()
129157
const userId = await getCurrentUserId()
130158
if (!userId) return
131159

132-
// Update individual question scores
133-
for (const qo of overrides.question_overrides) {
160+
let totalEarned = 0
161+
let totalPossible = 0
162+
163+
// Upsert individual question scores
164+
for (const [questionIdStr, data] of Object.entries(overrides)) {
165+
const questionId = parseInt(questionIdStr)
166+
const pointsPossible = questionData.find(q => q.question_id === questionId)?.points_possible || 10
167+
const pointsEarned = data.points_earned ?? 0
168+
totalEarned += pointsEarned
169+
totalPossible += pointsPossible
170+
134171
await supabase
135172
.from('exam_question_scores')
136173
.upsert({
137174
submission_id: parseInt(submissionId),
138-
question_id: qo.question_id,
139-
points_earned: qo.points_earned,
140-
points_possible: questionData.find(q => q.question_id === qo.question_id)?.points_possible || 10,
141-
is_correct: qo.is_correct,
175+
question_id: questionId,
176+
points_earned: pointsEarned,
177+
points_possible: pointsPossible,
178+
is_correct: data.is_correct ?? null,
142179
teacher_id: userId,
143-
teacher_notes: qo.teacher_notes,
144-
is_overridden: true,
180+
teacher_notes: data.teacher_notes || null,
181+
is_overridden: data.is_overridden || false,
145182
reviewed_at: new Date().toISOString(),
146183
}, { onConflict: 'submission_id,question_id' })
147184
}
148185

186+
const newScore = totalPossible > 0
187+
? Math.round((totalEarned / totalPossible) * 100)
188+
: (rawSubmission.score ?? 0)
189+
149190
// Update overall exam_scores
150191
await supabase
151192
.from('exam_scores')
152193
.upsert({
153194
submission_id: parseInt(submissionId),
154195
student_id: rawSubmission.student_id,
155196
exam_id: rawSubmission.exam_id,
156-
score: overrides.score,
157-
feedback: overrides.feedback,
197+
score: newScore,
158198
teacher_id: userId,
159-
teacher_notes: overrides.teacher_notes,
160199
is_overridden: true,
161200
reviewed_at: new Date().toISOString(),
162201
}, { onConflict: 'submission_id,student_id,exam_id' })
@@ -165,7 +204,7 @@ export default async function SubmissionDetailPage({ params }: { params: Promise
165204
await supabase
166205
.from('exam_submissions')
167206
.update({
168-
score: overrides.score,
207+
score: newScore,
169208
review_status: 'teacher_reviewed',
170209
requires_attention: false,
171210
})
@@ -190,7 +229,12 @@ export default async function SubmissionDetailPage({ params }: { params: Promise
190229
</div>
191230
</div>
192231

193-
<SubmissionReview submission={submission} onSave={handleSave} />
232+
<SubmissionReview
233+
submission={submission}
234+
questions={questionsForReview}
235+
answers={answersForReview}
236+
onSave={handleSave}
237+
/>
194238
</div>
195239
)
196240
}

app/[locale]/dashboard/teacher/courses/[courseId]/exams/[examId]/submissions/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,22 @@ export default async function SubmissionsPage({ params }: { params: Promise<{ co
5050
|| reviewStatus === 'pending_teacher_review'
5151
|| (submission.ai_confidence_score != null && submission.ai_confidence_score < 0.7)
5252

53+
// NOTE: field names below must match what ExamSubmissionsReview reads
54+
// (profiles.full_name, status, ai_score, final_score). They had drifted
55+
// (student_name / review_status / score), which rendered every row as
56+
// "Unknown Student" with "undefined%" scores and zeroed every stat card.
5357
return {
5458
id: submission.submission_id,
5559
student_id: submission.student_id,
56-
student_name: student?.full_name || t('manageCourse.studentList.unknownStudent'),
60+
profiles: {
61+
full_name: student?.full_name || null,
62+
},
63+
status: reviewStatus,
5764
submitted_at: submission.submission_date,
58-
score: submission.score || 0,
59-
review_status: reviewStatus as 'pending' | 'pending_teacher_review' | 'ai_reviewed' | 'teacher_reviewed',
65+
// DB has a single `score` column; AI grade and final score are the same
66+
// until a teacher override updates `score`.
67+
ai_score: submission.score ?? null,
68+
final_score: submission.score ?? null,
6069
requires_attention: requiresAttention,
6170
ai_model_used: submission.ai_model_used || undefined,
6271
ai_processing_time_ms: submission.ai_processing_time_ms || undefined,
519 KB
Loading

0 commit comments

Comments
 (0)