diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx
index 4abe06117..ff8759458 100644
--- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx
+++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx
@@ -22,6 +22,7 @@ import {
TextInput,
ThemeIcon,
Box,
+ Tooltip,
rem,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
@@ -48,6 +49,7 @@ import {
useUpdateExamMutation,
useAssignQuestionsMutation,
useSelectRandomQuestionsMutation,
+ useGetExamRegistrationsQuery,
} from '../api/exam-api';
import { useGetQuestionsQuery } from '../../question/api/question-api';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
@@ -114,6 +116,12 @@ export function ExamDetailPage() {
const [selectRandom, { isLoading: isDrawing }] = useSelectRandomQuestionsMutation();
const { data: exam, isLoading, isError } = useGetExamQuery(id ?? '', { skip: !id });
+ // The backend locks the paper the moment the first candidate registers
+ // (ExamService.assertPaperEditable) — every candidate must sit the same
+ // paper. Same query ExamCandidatesPanel already runs, so RTK Query serves
+ // it from cache rather than issuing a second request.
+ const { data: registrations } = useGetExamRegistrationsQuery(id ?? '', { skip: !id });
+ const paperLocked = (registrations?.length ?? 0) > 0;
const { data: qRes } = useGetQuestionsQuery();
const { data: certRes } = useGetCertificationsQuery();
const allQuestions = qRes?.items ?? [];
@@ -183,7 +191,9 @@ export function ExamDetailPage() {
} catch (error) {
const key = extractErrorMessage(error, t('exam.randomError'));
notify.error(
- key.startsWith('insufficient_approved_questions')
+ key === 'paper_locked_after_registration'
+ ? t('exam.paperLockedHint', { count: registrations?.length ?? 0 })
+ : key.startsWith('insufficient_approved_questions')
? `${t('exam.notEnoughApproved')} (${key.split(':')[1] ?? ''})`
: key.startsWith('paper_cannot_reach_cutting_point')
? t('exam.cannotReachCuttingPoint', {
@@ -204,7 +214,9 @@ export function ExamDetailPage() {
} catch (error) {
const key = extractErrorMessage(error, 'Failed to assign questions');
notify.error(
- key.startsWith('question_not_approved')
+ key === 'paper_locked_after_registration'
+ ? t('exam.paperLockedHint', { count: registrations?.length ?? 0 })
+ : key.startsWith('question_not_approved')
? t('question.qc.onlyApprovedUsable')
: key.startsWith('paper_cannot_reach_cutting_point')
? t('exam.cannotReachCuttingPoint', {
@@ -456,19 +468,45 @@ export function ExamDetailPage() {
{t("exam.detail.questionsSection", { pts: totalPoints })}
- }
- onClick={openAssignModal}
- >
- {t("exam.manageQuestions")}
-
+
+ {paperLocked && (
+
+ {t("exam.paperLocked")}
+
+ )}
+
+ {/* Wrapped: a disabled Mantine Button fires no pointer events,
+ so the tooltip needs an enabled element to hang off. */}
+
+ }
+ onClick={openAssignModal}
+ disabled={paperLocked}
+ >
+ {t("exam.manageQuestions")}
+
+
+
+
{(exam.questions ?? []).length === 0 ? (
- }>
- {t("exam.noQuestionsAssigned")}
+ }
+ >
+ {paperLocked
+ ? t("exam.paperLockedHint", { count: registrations?.length ?? 0 })
+ : t("exam.noQuestionsAssigned")}
) : (
diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts
index 292a374ae..c8e51193d 100644
--- a/apps/backoffice/src/app/i18n/locales/am.ts
+++ b/apps/backoffice/src/app/i18n/locales/am.ts
@@ -387,6 +387,9 @@ export const am: Translations = {
notEnoughApproved: "ለዚህ ትምህርት በቂ የጸደቁ ጥያቄዎች የሉም።",
cannotReachCuttingPoint:
"ይህ ወረቀት የማለፊያ ነጥቡን ሊደርስ አይችልም (ከፍተኛ {{max}}፣ የማለፊያ ነጥብ {{cuttingPoint}})። ተጨማሪ ወይም ከፍ ያለ ነጥብ ያላቸው ጥያቄዎችን ጨምር፣ ወይም የማለፊያ ነጥቡን ቀንስ።",
+ paperLocked: "ወረቀቱ ተቆልፏል",
+ paperLockedHint:
+ "ለዚህ ፈተና {{count}} ተፈታኝ(ዎች) ተመዝግበዋል። ሁሉም ተፈታኞች አንድ ዓይነት ወረቀት መፈተን ስላለባቸው ጥያቄዎቹ ከዚህ በኋላ አይቀየሩም።",
},
country: {
diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts
index 8c0ff66ff..7152067ce 100644
--- a/apps/backoffice/src/app/i18n/locales/en.ts
+++ b/apps/backoffice/src/app/i18n/locales/en.ts
@@ -386,6 +386,9 @@ export const en = {
'Not enough approved questions in the bank for this subject.',
cannotReachCuttingPoint:
'This paper cannot reach the passing mark (max {{max}}, pass mark {{cuttingPoint}}). Add more/higher-point questions, or lower the cutting point.',
+ paperLocked: 'Paper locked',
+ paperLockedHint:
+ '{{count}} candidate(s) have registered for this session. Every candidate must sit the same paper, so questions can no longer be changed.',
},
country: {