Merge branch 'feature/exam-attempt-domain' of github.com:Tria-plc/emaui into feature/exam-attempt-domain

This commit is contained in:
mihretue
2026-08-17 13:41:39 +03:00
5 changed files with 62 additions and 10 deletions

View File

@@ -20,7 +20,9 @@ const examApi = baseApi.injectEndpoints({
providesTags: ['Api'], providesTags: ['Api'],
}), }),
getExam: builder.query<Exam, string>({ getExam: builder.query<Exam, string>({
query: (id) => `/exams/${id}?i=questions`, // Nested relation so CHOICE questions carry their options here too —
// needed to print real answer choices instead of blank A/B/C/D lines.
query: (id) => `/exams/${id}?i=questions,questions.options`,
providesTags: ['Api'], providesTags: ['Api'],
}), }),
createExam: builder.mutation<Exam, CreateExamPayload>({ createExam: builder.mutation<Exam, CreateExamPayload>({

View File

@@ -174,7 +174,12 @@ export function ExamDetailPage() {
notify.error( notify.error(
key.startsWith('insufficient_approved_questions') key.startsWith('insufficient_approved_questions')
? `${t('exam.notEnoughApproved')} (${key.split(':')[1] ?? ''})` ? `${t('exam.notEnoughApproved')} (${key.split(':')[1] ?? ''})`
: key, : key.startsWith('paper_cannot_reach_cutting_point')
? t('exam.cannotReachCuttingPoint', {
max: key.split(':')[1]?.split('/')[0] ?? '',
cuttingPoint: key.split(':')[1]?.split('/')[1] ?? '',
})
: key,
); );
} }
}; };
@@ -190,18 +195,35 @@ export function ExamDetailPage() {
notify.error( notify.error(
key.startsWith('question_not_approved') key.startsWith('question_not_approved')
? t('question.qc.onlyApprovedUsable') ? t('question.qc.onlyApprovedUsable')
: key, : key.startsWith('paper_cannot_reach_cutting_point')
? t('exam.cannotReachCuttingPoint', {
max: key.split(':')[1]?.split('/')[0] ?? '',
cuttingPoint: key.split(':')[1]?.split('/')[1] ?? '',
})
: key,
); );
} }
}; };
const handlePrint = async () => { const handlePrint = async () => {
const total = (exam.questions ?? []).reduce( // The reachable max depends on the evaluation method, not the raw point
(s, q) => s + Number(q.points), // sum — mirrors RecordResultModal's grading math so "can this paper pass"
0, // means the same thing here as it does at marking time. Cutting point can
); // be raised after the paper was assembled (edit modal, no re-check on
if (total < Number(exam.cuttingPoint)) { // save), so this still needs to run even though assignment now enforces
// it too.
const questions = exam.questions ?? [];
const total = questions.reduce((s, q) => s + Number(q.points), 0);
const reachableMax =
exam.evaluationMethod === 'AVERAGE'
? questions.length
? total / questions.length
: 0
: exam.evaluationMethod === 'PERCENTAGE'
? 100
: total;
if (reachableMax < Number(exam.cuttingPoint)) {
notify.error( notify.error(
`Total question marks (${total}) is less than the passing mark (${exam.cuttingPoint}). Add more questions or adjust the cutting point before printing.`, `This paper cannot reach the passing mark under its ${EVAL_LABEL[exam.evaluationMethod] ?? exam.evaluationMethod} evaluation (max ${reachableMax}, pass mark ${exam.cuttingPoint}). Add more questions or adjust the cutting point before printing.`,
); );
return; return;
} }
@@ -235,7 +257,23 @@ export function ExamDetailPage() {
<p style="margin: 0 0 4px 0; font-size: 14px; line-height: 1.5;">${titleStr}</p> <p style="margin: 0 0 4px 0; font-size: 14px; line-height: 1.5;">${titleStr}</p>
${descStr ? `<p style="margin: 0 0 8px 0; font-size: 12px; color: #555; line-height: 1.4;">${descStr}</p>` : ""} ${descStr ? `<p style="margin: 0 0 8px 0; font-size: 12px; color: #555; line-height: 1.4;">${descStr}</p>` : ""}
${q.form === "ESSAY" ? '<div style="border-bottom: 1px dashed #ccc; height: 80px; margin-bottom: 12px;"></div>'.repeat(3) : ""} ${q.form === "ESSAY" ? '<div style="border-bottom: 1px dashed #ccc; height: 80px; margin-bottom: 12px;"></div>'.repeat(3) : ""}
${q.form === "CHOICE" ? ["A. ______", "B. ______", "C. ______", "D. ______"].map((l) => `<p style="margin: 4px 0; font-size: 13px;">${l}</p>`).join("") : ""} ${
q.form === "CHOICE"
? q.options && q.options.length
? q.options
.slice()
.sort((a, b) => a.order - b.order)
.map(
(o, oi) =>
`<p style="margin: 4px 0; font-size: 13px;">${String.fromCharCode(65 + oi)}. ${o.text[locale] || o.text.en}</p>`,
)
.join("")
// No options on record (legacy question, or options relation
// wasn't loaded) — fall back to blank lines rather than
// printing nothing.
: ["A. ______", "B. ______", "C. ______", "D. ______"].map((l) => `<p style="margin: 4px 0; font-size: 13px;">${l}</p>`).join("")
: ""
}
</div>`; </div>`;
}) })
.join(""); .join("");

View File

@@ -15,11 +15,19 @@ export type ExamStatus =
| "POSTPONED" | "POSTPONED"
| "PUBLISHED"; | "PUBLISHED";
/** Only populated when the exam is fetched with `?i=questions,questions.options`. */
export interface QuestionOptionBrief {
id: string;
text: LocalePair;
order: number;
}
export interface QuestionBrief { export interface QuestionBrief {
id: string; id: string;
title: LocalePair; title: LocalePair;
form: QuestionForm; form: QuestionForm;
points: number; points: number;
options?: QuestionOptionBrief[];
} }
export interface Exam { export interface Exam {

View File

@@ -368,6 +368,8 @@ export const am: Translations = {
randomSelected: "{{count}} የጸደቁ ጥያቄዎች ተመርጠዋል", randomSelected: "{{count}} የጸደቁ ጥያቄዎች ተመርጠዋል",
randomError: "ጥያቄዎችን መምረጥ አልተቻለም", randomError: "ጥያቄዎችን መምረጥ አልተቻለም",
notEnoughApproved: "ለዚህ ትምህርት በቂ የጸደቁ ጥያቄዎች የሉም።", notEnoughApproved: "ለዚህ ትምህርት በቂ የጸደቁ ጥያቄዎች የሉም።",
cannotReachCuttingPoint:
"ይህ ወረቀት የማለፊያ ነጥቡን ሊደርስ አይችልም (ከፍተኛ {{max}}፣ የማለፊያ ነጥብ {{cuttingPoint}})። ተጨማሪ ወይም ከፍ ያለ ነጥብ ያላቸው ጥያቄዎችን ጨምር፣ ወይም የማለፊያ ነጥቡን ቀንስ።",
}, },
country: { country: {

View File

@@ -366,6 +366,8 @@ export const en = {
randomError: 'Could not draw questions', randomError: 'Could not draw questions',
notEnoughApproved: notEnoughApproved:
'Not enough approved questions in the bank for this subject.', '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.',
}, },
country: { country: {