From 9f8f29d61478384d56ac530cc1ab6325a4157782 Mon Sep 17 00:00:00 2001 From: mihretue Date: Mon, 24 Aug 2026 11:30:56 +0000 Subject: [PATCH 01/14] fix(exam): stop hiding non-CHOICE questions from OFFLINE assignment picker eligibleQuestions filtered by q.form === exam.form, which matched the backend's own restriction (assertUsable) only for ONLINE exams. Once exam.form gained the "BOTH" value for OFFLINE mixed papers, no question ever has form "BOTH", so the picker silently emptied out or, for a plain CHOICE-form exam, looked CHOICE-only regardless of administration method. Now mirrors the backend: CHOICE-only gate applies only when administrationMethod is ONLINE. --- .../src/app/features/exam/pages/ExamDetailPage.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx index 8e9037840..5ce7a4ab5 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx @@ -121,18 +121,25 @@ export function ExamDetailPage() { // Only approved bank items may go on a paper (US-EXAM-003), so the picker // must not offer drafts or retired questions either. + // + // Form restriction mirrors the backend's assertUsable: ONLINE exams are + // CHOICE-only (auto-grading needs it), OFFLINE exams have no form + // restriction at all — mixing ESSAY and CHOICE by hand (exam.form + // "BOTH") is the sanctioned, only way to build a mixed paper. Matching + // `q.form === exam.form` here used to hide every question once "BOTH" + // became a real exam.form value, since no question itself is "BOTH". const eligibleQuestions = useMemo(() => { if (!exam) return []; return allQuestions .filter( (q) => q.certificationId === exam.certificationId && - q.form === exam.form && - q.status === 'APPROVED', + q.status === 'APPROVED' && + (exam.administrationMethod !== 'ONLINE' || q.form === 'CHOICE'), ) .map((q) => ({ id: q.id, title: q.title, form: q.form, points: q.points })); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allQuestions, exam?.certificationId, exam?.form]); + }, [allQuestions, exam?.certificationId, exam?.administrationMethod]); if (isLoading) return ; From 35961ec8099a9712254545a2cbbc56ea92581023 Mon Sep 17 00:00:00 2001 From: mihretue Date: Mon, 24 Aug 2026 12:57:37 +0000 Subject: [PATCH 02/14] feat(exam): let ONLINE exams take ESSAY/BOTH form, add grading-sheet API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend half of the backend change: ExamPage no longer disables the form Select or force-resets it to CHOICE when administrationMethod is ONLINE (backend's assertOnlineIsChoiceOnly is gone, matching gate removed here). Also wires up GET .../grading-sheet as useGetGradingSheetQuery — RecordResultModal wiring (show candidate answers, prefill auto-computed CHOICE scores) is next. --- .../src/app/features/exam/api/exam-api.ts | 11 +++++++++++ .../app/features/exam/pages/ExamPage/index.tsx | 15 +-------------- .../src/app/features/exam/types/exam.ts | 18 ++++++++++++++++++ apps/backoffice/src/app/i18n/locales/am.ts | 1 - apps/backoffice/src/app/i18n/locales/en.ts | 1 - 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/backoffice/src/app/features/exam/api/exam-api.ts b/apps/backoffice/src/app/features/exam/api/exam-api.ts index d313a38ac..4dbe8f713 100644 --- a/apps/backoffice/src/app/features/exam/api/exam-api.ts +++ b/apps/backoffice/src/app/features/exam/api/exam-api.ts @@ -12,6 +12,7 @@ import type { CreateIncidentPayload, ResolveIncidentPayload, RegradeOutcome, + GradingSheet, } from '../types/exam'; const examApi = baseApi.injectEndpoints({ @@ -101,6 +102,15 @@ const examApi = baseApi.injectEndpoints({ }), invalidatesTags: ['Api'], }), + /** The candidate's answers plus auto-computable CHOICE scores, for RecordResultModal. */ + getGradingSheet: builder.query< + GradingSheet, + { examId: string; profileId: string } + >({ + query: ({ examId, profileId }) => + `/exam-attempts/exam/${examId}/candidate/${profileId}/grading-sheet`, + providesTags: ['Api'], + }), }), overrideExisting: false, }); @@ -119,4 +129,5 @@ export const { useRecordIncidentMutation, useResolveIncidentMutation, useRegradeAttemptMutation, + useGetGradingSheetQuery, } = examApi; diff --git a/apps/backoffice/src/app/features/exam/pages/ExamPage/index.tsx b/apps/backoffice/src/app/features/exam/pages/ExamPage/index.tsx index 034d203ae..34039328c 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamPage/index.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamPage/index.tsx @@ -255,12 +255,6 @@ function ExamForm({ onChange={setForm} size="sm" required - disabled={adminMethod === "ONLINE"} - description={ - adminMethod === "ONLINE" - ? t("exam.form.onlineChoiceOnlyHint") - : undefined - } />