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 })} - + + {paperLocked && ( + + {t("exam.paperLocked")} + + )} + + {/* Wrapped: a disabled Mantine Button fires no pointer events, + so the tooltip needs an enabled element to hang off. */} + + + + + {(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: {