The COC detail page read the application status alone, so a candidate who had
registered, been marked present, sat the paper and had a PASSED mark published
still appeared to be awaiting an exam — and the officer was offered "Schedule
exam" and "Record exam result" for work already done.
ExamStatePanel reads GET /exams/applications/:id/state, which the server
derives from the registration, the attempt and the published mark. Session,
date and admission number while the exam is ahead; score, outcome and whether
the exam engine or an examiner produced the mark once it is settled. Polled,
because attendance is recorded and papers are marked while the officer has the
page open. Same derivation the applicant's portal reads, so the two cannot
disagree.
The eligible-exams query, the scheduling mutation and the outcome mutation are
gone from the RTK layer along with the endpoints behind them.
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.
The registrations table mirrored the server's old attendance deny-list —
ABSENT/WITHDRAWN/DISQUALIFIED — so the default REGISTERED fell through and
"Take exam" appeared before any invigilator had confirmed the candidate was
there. It is an allow-list now, PRESENT and LATE only, matching
ExamAttemptService.MAY_SIT.
A blocked candidate gets a reason rather than an empty cell: "Awaiting
attendance" with a tooltip while nobody has called the register, "Not sitting"
once attendance was taken and they are not. useExamAttempt does the same for
the direct-link path, and translates attendance_not_confirmed /
candidate_not_present instead of showing a raw error key on a Start button
that cannot work. The server refuses either way — this only makes the refusal
legible.
ScheduleExamModal no longer promises an admission number: scheduling makes a
sitting available, and the candidate registers for it themselves.
refactor(i18n): remove 'assignReviewer' translation key from Amharic and English locales
refactor(nav-config): clean up navigation configuration by removing redundant items
refactor(licensing-api): remove assignReviewer mutation from licensing API
feat(package): add build script for parallel building of portal and backoffice
Submitting from Basic Info complained that required fields on the Settings
tab were empty — a correct error the user could not act on, since Settings
had not been shown yet.
The primary action now reads "Next" while Basic Info is open and validates
only that tab before moving on; Create/Update appears on Settings, the final
step, alongside a Back button. Submit still checks both tabs, since the tab
headers stay clickable and a user can reach Settings without going through
Next.
The detail page fired assign/draw requests blind and surfaced the raw
backend key to the user — a locked paper showed up as the literal string
"paper_locked_after_registration" in a toast.
Manage Questions is now disabled once any candidate has registered, with a
tooltip and badge explaining why; both the manual assign and the random draw
live behind that modal, so one guard covers the same ground the backend's
assertPaperEditable does. The empty-paper alert now explains the deadlock
instead of telling the user to click a button that will fail, and both
error handlers translate paper_locked_after_registration in case someone
registers while the modal is open.
Registrations come from the query ExamCandidatesPanel already runs on this
page, so RTK Query serves it from cache rather than issuing a second request.
Wires useGetGradingSheetQuery into the grading modal: a new 'Candidate's
Answer' column shows the selected option or essay text per question, and
whichever CHOICE scores auto-grading could already compute get prefilled
into the score inputs (tagged 'Auto-graded', still editable — the examiner
can override). Grading a mixed or ESSAY paper no longer means guessing at
what was answered or re-deriving the CHOICE portion by eye.
Degrades to blank inputs exactly as before for an OFFLINE candidate or one
who hasn't sat an online attempt (gradingSheet returns empty answers for
those, not an error).
Frontend half of the backend change: ExamPage no longer disables the
form Select or force-resets it to CHOICE when administrationMethod is
ONLINE (backend's assertOnlineIsChoiceOnly is gone, matching gate
removed here). Also wires up GET .../grading-sheet as
useGetGradingSheetQuery — RecordResultModal wiring (show candidate
answers, prefill auto-computed CHOICE scores) is next.
eligibleQuestions filtered by q.form === exam.form, which matched the
backend's own restriction (assertUsable) only for ONLINE exams. Once
exam.form gained the "BOTH" value for OFFLINE mixed papers, no question
ever has form "BOTH", so the picker silently emptied out or, for a
plain CHOICE-form exam, looked CHOICE-only regardless of administration
method. Now mirrors the backend: CHOICE-only gate applies only when
administrationMethod is ONLINE.