mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Merge branch 'feature/exam-attempt-domain' of github.com:Tria-plc/emaui into feature/exam-attempt-domain
This commit is contained in:
@@ -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>({
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -368,6 +368,8 @@ export const am: Translations = {
|
|||||||
randomSelected: "{{count}} የጸደቁ ጥያቄዎች ተመርጠዋል",
|
randomSelected: "{{count}} የጸደቁ ጥያቄዎች ተመርጠዋል",
|
||||||
randomError: "ጥያቄዎችን መምረጥ አልተቻለም",
|
randomError: "ጥያቄዎችን መምረጥ አልተቻለም",
|
||||||
notEnoughApproved: "ለዚህ ትምህርት በቂ የጸደቁ ጥያቄዎች የሉም።",
|
notEnoughApproved: "ለዚህ ትምህርት በቂ የጸደቁ ጥያቄዎች የሉም።",
|
||||||
|
cannotReachCuttingPoint:
|
||||||
|
"ይህ ወረቀት የማለፊያ ነጥቡን ሊደርስ አይችልም (ከፍተኛ {{max}}፣ የማለፊያ ነጥብ {{cuttingPoint}})። ተጨማሪ ወይም ከፍ ያለ ነጥብ ያላቸው ጥያቄዎችን ጨምር፣ ወይም የማለፊያ ነጥቡን ቀንስ።",
|
||||||
},
|
},
|
||||||
|
|
||||||
country: {
|
country: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user