= { ESSAY: "Essay", CHOICE: "Choice" };
@@ -171,7 +172,12 @@ export function ExamDetailPage() {
notify.error(
key.startsWith('insufficient_approved_questions')
? `${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,
);
}
};
@@ -187,18 +193,35 @@ export function ExamDetailPage() {
notify.error(
key.startsWith('question_not_approved')
? 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 total = (exam.questions ?? []).reduce(
- (s, q) => s + Number(q.points),
- 0,
- );
- if (total < Number(exam.cuttingPoint)) {
+ // The reachable max depends on the evaluation method, not the raw point
+ // sum — mirrors RecordResultModal's grading math so "can this paper pass"
+ // 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
+ // 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(
- `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;
}
@@ -232,7 +255,23 @@ export function ExamDetailPage() {
${titleStr}
${descStr ? `${descStr}
` : ""}
${q.form === "ESSAY" ? ''.repeat(3) : ""}
- ${q.form === "CHOICE" ? ["A. ______", "B. ______", "C. ______", "D. ______"].map((l) => `${l}
`).join("") : ""}
+ ${
+ q.form === "CHOICE"
+ ? q.options && q.options.length
+ ? q.options
+ .slice()
+ .sort((a, b) => a.order - b.order)
+ .map(
+ (o, oi) =>
+ `${String.fromCharCode(65 + oi)}. ${o.text[locale] || o.text.en}
`,
+ )
+ .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) => `${l}
`).join("")
+ : ""
+ }
`;
})
.join("");
@@ -247,7 +286,20 @@ export function ExamDetailPage() {
.header p { margin: 2px 0; font-size: 13px; color: #555; }
.directions { background: #f5f5f5; padding: 12px 16px; border-radius: 4px; margin-bottom: 24px; font-size: 13px; }
.directions strong { display: block; margin-bottom: 4px; }
- @media print { @page { margin: 20mm; } body { -webkit-print-color-adjust: exact; } }
+ .footer { margin-top: 40px; border-top: 1px solid #ccc; padding-top: 12px; font-size: 12px; color: #888; text-align: center; }
+ /* Pinned to the bottom of every printed page (not just after the
+ last question) — @page's bottom margin leaves room for it so it
+ never overlaps question text on the last page. */
+ @media print {
+ @page { margin: 20mm 20mm 28mm 20mm; }
+ /* @page's margin already insets content from the physical page
+ edge — body's own 40px padding (needed on-screen, for the
+ preview tab before printing) would double up with it here,
+ wasting real page height on every side and fitting noticeably
+ fewer questions per page than the paper actually has room for. */
+ body { -webkit-print-color-adjust: exact; padding: 0; max-width: none; }
+ .footer { position: fixed; bottom: 0; left: 0; right: 0; margin-top: 0; }
+ }
${exam.direction?.[locale] ? `Directions: ${exam.direction[locale]}
` : ""}
${qHtml}
-
+