mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 16:35:43 +00:00
Four screens were telling the applicant things that were not true, and two back-office actions were writing state nobody should be able to write by hand. The exam form is a stepper. Basic Info and Settings each answer only for their own fields, and a third step reviews the whole thing before anything is sent — "Create Exam" exists on that step alone, so there is no path from an earlier one into the API. Clicking it from Basic Info used to report the Settings fields as missing: a correct error the user could not act on, since that step had not been shown yet. The rules move to ./validation, which is what the per-step check and the final whole-payload check both read, so the two cannot drift apart. Back keeps everything entered — state lives in the component. The portal derives the examination stage from the records rather than from the application status alone. Registering, attendance and the sitting itself all happen on the registration and the attempt, and the portal read none of them — which is how an application still said "Awaiting Exam Date" after the back office had already evaluated the paper. examStageFor() names what the records add up to: eligible to register, registered, attendance confirmed, sitting, under evaluation, passed, failed. Both the applications list and the certificates page use it, and both poll the registrations they read it from, because attendance is recorded by an invigilator while the candidate is watching the screen. EXAM_PAID is no longer a waiting state. Nobody assigns a date: the fee having cleared is exactly what makes the candidate eligible to pick a published sitting, so that row now offers Register rather than a disabled button. Schedule Exam and Record Exam Outcome are gone from the COC queue — actions, modals, RTK endpoints and strings. Their server endpoints are gone too, so this is not a hidden button over a live route.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 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;
|
|
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';
|
|
}
|
|
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;
|
|
}
|