diff --git a/apps/backoffice/src/app/features/result/components/RecordResultModal/columns.tsx b/apps/backoffice/src/app/features/result/components/RecordResultModal/columns.tsx index f224aa828..9503b1120 100644 --- a/apps/backoffice/src/app/features/result/components/RecordResultModal/columns.tsx +++ b/apps/backoffice/src/app/features/result/components/RecordResultModal/columns.tsx @@ -1,7 +1,7 @@ -import { NumberInput, Text, TextInput } from '@mantine/core'; +import { Badge, Group, NumberInput, Text, TextInput } from '@mantine/core'; import type { TFunction } from 'i18next'; import type { AdvancedColumn } from '@ema-platform/ui'; -import type { QuestionBrief } from '../../../exam/types/exam'; +import type { GradingSheetQuestion, QuestionBrief } from '../../../exam/types/exam'; export function recordResultColumns( t: TFunction, @@ -11,6 +11,9 @@ export function recordResultColumns( questionRemarks: Record; onScoreChange: (questionId: string, value: number) => void; onRemarkChange: (questionId: string, value: string) => void; + /** The candidate's own answer + auto-score, when available (empty for + * an OFFLINE candidate or one who hasn't sat an online attempt). */ + answersByQuestion: Map; }, ): AdvancedColumn[] { return [ @@ -22,6 +25,20 @@ export function recordResultColumns( ), }, + { + header: t('result.recordModal.candidateAnswer'), + cell: ({ row }) => { + const answer = handlers.answersByQuestion.get(row.original.id); + if (!answer || (!answer.answerText && !answer.selectedOptionText)) { + return {t('result.recordModal.noAnswer')}; + } + return ( + + {answer.selectedOptionText?.[locale] ?? answer.answerText} + + ); + }, + }, { header: t('result.recordModal.maxPoints'), cell: ({ row }) => ( @@ -32,16 +49,26 @@ export function recordResultColumns( }, { header: t('result.recordModal.score'), - cell: ({ row }) => ( - handlers.onScoreChange(row.original.id, Number(v))} - min={0} - max={row.original.points} - size="xs" - style={{ width: 80 }} - /> - ), + cell: ({ row }) => { + const autoGraded = handlers.answersByQuestion.get(row.original.id)?.autoScore != null; + return ( + + handlers.onScoreChange(row.original.id, Number(v))} + min={0} + max={row.original.points} + size="xs" + style={{ width: 80 }} + /> + {autoGraded && ( + + {t('result.recordModal.autoGraded')} + + )} + + ); + }, }, { header: t('result.recordModal.remark'), diff --git a/apps/backoffice/src/app/features/result/components/RecordResultModal/index.tsx b/apps/backoffice/src/app/features/result/components/RecordResultModal/index.tsx index 15ed50335..b073b1ca1 100644 --- a/apps/backoffice/src/app/features/result/components/RecordResultModal/index.tsx +++ b/apps/backoffice/src/app/features/result/components/RecordResultModal/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, @@ -20,7 +20,7 @@ import { extractErrorMessage } from '@ema-platform/api'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; import { recordResultColumns } from './columns'; import { useCreateResultMutation } from '../../api/result-api'; -import { useGetExamRegistrationsQuery } from '../../../exam/api/exam-api'; +import { useGetExamRegistrationsQuery, useGetGradingSheetQuery } from '../../../exam/api/exam-api'; import type { Exam } from '../../../exam/types/exam'; function InfoRow({ label, value }: { label: string; value: string }) { @@ -55,12 +55,39 @@ export function RecordResultModal({ const { data: registrations } = useGetExamRegistrationsQuery(exam.id, { skip: !opened, }); + // The candidate's own answers plus whatever score auto-grading could + // already compute for the CHOICE portion — degrades to "no data" for an + // OFFLINE candidate or one who never sat an online attempt, same as + // before this existed. + const { data: gradingSheet } = useGetGradingSheetQuery( + { examId: exam.id, profileId: selectedSeafarerId ?? '' }, + { skip: !opened || !selectedSeafarerId }, + ); + const answersByQuestion = new Map( + (gradingSheet?.questions ?? []).map((q) => [q.questionId, q]), + ); const [createResult, { isLoading: isSaving }] = useCreateResultMutation(); const table = useServerTable(); const questions = exam.questions ?? []; const pagedQuestions = table.paginate(questions); + // Prefill (never override) the CHOICE questions auto-grading already + // scored — the examiner only has to key in the ESSAY marks. A fresh + // seafarer selection always starts from an empty scores map, so this + // only ever fills in blanks, never stomps a manual edit already made. + useEffect(() => { + if (!gradingSheet) return; + const autoScores: Record = {}; + for (const q of gradingSheet.questions) { + if (q.autoScore !== null) autoScores[q.questionId] = q.autoScore; + } + if (Object.keys(autoScores).length) { + setScores((prev) => ({ ...autoScores, ...prev })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [gradingSheet]); + const seafarerOptions = (registrations ?? []) .filter((registration) => ['PRESENT', 'LATE'].includes(registration.attendanceStatus), @@ -175,6 +202,7 @@ export function RecordResultModal({ questionRemarks, onScoreChange: handleScoreChange, onRemarkChange: handleQuestionRemarkChange, + answersByQuestion, })} data={pagedQuestions.rows} itemCount={pagedQuestions.itemCount} diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts index 1e75f20eb..292a374ae 100644 --- a/apps/backoffice/src/app/i18n/locales/am.ts +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -657,6 +657,9 @@ export const am: Translations = { seafarerPlaceholder: "መርከበኛ ይፈልጉ እና ይምረጡ", scorePerQuestion: "በጥያቄ ውጤት", question: "ጥያቄ", + candidateAnswer: "የተፈታኙ መልስ", + noAnswer: "የተመዘገበ መልስ የለም", + autoGraded: "በራስ-ሰር የተመዘነ", maxPoints: "ከፍተኛ ውጤት", score: "ውጤት", remark: "ማስታወሻ", diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts index 3af7597f0..8c0ff66ff 100644 --- a/apps/backoffice/src/app/i18n/locales/en.ts +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -657,6 +657,9 @@ export const en = { seafarerPlaceholder: 'Search and select a seafarer', scorePerQuestion: 'Score per Question', question: 'Question', + candidateAnswer: "Candidate's Answer", + noAnswer: 'No answer on file', + autoGraded: 'Auto-graded', maxPoints: 'Max Points', score: 'Score', remark: 'Remark',