mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 20:05:42 +00:00
- COC queue status cell shows the server-derived exam state (registered, present, sat, passed, failed) instead of the lagging application status. - Portal examStageFor prefers the server's examState so portal and back office never disagree; NOT_SITTING stage added. - Exam roster shows each candidate's result and lock; Regrade hidden once a result exists. - Record Result modal: only unmarked candidates, empty score boxes (no silent zeros), reason required, backend refusals translated. - Result page: auto-graded marks read-only, per-applicant publish. - Exam page: session window fields, Add Question menu (bank / Excel / scratch), wait metrics panel; PUBLISHED exam status removed. - Portal: exam window shown, early launch and eligibility refusals explained. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
/**
|
|
* Which step owns which required fields.
|
|
*
|
|
* Extracted from the form so the rule "Next checks only the step in front of
|
|
* the user" is testable without rendering a modal — and so the final submit
|
|
* and the per-step check can never drift apart, since both read this.
|
|
*/
|
|
export interface ExamFormValues {
|
|
certificationId: string | null;
|
|
titleEn: string;
|
|
titleAm: string;
|
|
directionEn: string;
|
|
directionAm: string;
|
|
date: string;
|
|
/** `HH:MM` or empty — the session window, optional on both ends. */
|
|
startTime: string;
|
|
endTime: string;
|
|
venue: string;
|
|
type: string | null;
|
|
form: string | null;
|
|
adminMethod: string | null;
|
|
evalMethod: string | null;
|
|
cuttingPoint: number;
|
|
}
|
|
|
|
/** Basic Info, Settings, Review — only the last one submits. */
|
|
export const LAST_STEP = 2;
|
|
|
|
export function validateBasic(v: ExamFormValues): string | null {
|
|
if (!v.certificationId || !v.titleEn || !v.titleAm || !v.date || !v.venue) {
|
|
return 'exam.form.fillRequiredBasic';
|
|
}
|
|
// Either both languages or neither: a direction in one language only reads
|
|
// as missing to half the candidates.
|
|
if ((v.directionEn || v.directionAm) && !(v.directionEn && v.directionAm)) {
|
|
return 'exam.form.directionBothLanguages';
|
|
}
|
|
// A session cannot close before it opens. Same rule the API applies
|
|
// (exam_window_invalid), caught here so it is reported on the step that
|
|
// owns the fields.
|
|
if (v.startTime && v.endTime && v.endTime <= v.startTime) {
|
|
return 'exam.form.windowInvalid';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function validateSettings(v: ExamFormValues): string | null {
|
|
return !v.type || !v.form || !v.adminMethod || !v.evalMethod || !v.cuttingPoint
|
|
? 'exam.form.fillRequiredSettings'
|
|
: null;
|
|
}
|
|
|
|
/** The step in front of the user, and nothing else. Review has no fields. */
|
|
export function validateStep(step: number, v: ExamFormValues): string | null {
|
|
if (step === 0) return validateBasic(v);
|
|
if (step === 1) return validateSettings(v);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The whole payload, as the final check before submitting.
|
|
*
|
|
* Not redundant with the per-step checks: the stepper header can be clicked
|
|
* back to a visited step, so arriving at Review is no proof the user walked
|
|
* here in order with everything still filled in.
|
|
*/
|
|
export function validateAll(v: ExamFormValues): string | null {
|
|
return validateBasic(v) ?? validateSettings(v);
|
|
}
|
|
|
|
/** Where a failure has to land for the user to be able to fix it. */
|
|
export function stepOfError(error: string): number {
|
|
return error === 'exam.form.fillRequiredSettings' ? 1 : 0;
|
|
}
|