Refactor exam form handling to support mixed paper types and update related interfaces

This commit is contained in:
Nati
2026-08-24 12:37:06 +00:00
parent 895690de80
commit 294a8ded6c
3 changed files with 19 additions and 14 deletions

View File

@@ -117,13 +117,18 @@ export function ExamDetailPage() {
// Only approved bank items may go on a paper (US-EXAM-003), so the picker // Only approved bank items may go on a paper (US-EXAM-003), so the picker
// must not offer drafts or retired questions either. // must not offer drafts or retired questions either.
//
// BOTH describes a mixed paper — a question itself is never "BOTH" (see
// QuestionForm), so an equality check against it would match nothing and
// silently offer zero questions. Same skip-condition as the backend's own
// random draw (ExamService.selectRandomQuestions).
const eligibleQuestions = useMemo(() => { const eligibleQuestions = useMemo(() => {
if (!exam) return []; if (!exam) return [];
return allQuestions return allQuestions
.filter( .filter(
(q) => (q) =>
q.certificationId === exam.certificationId && q.certificationId === exam.certificationId &&
q.form === exam.form && (exam.form === 'BOTH' || q.form === exam.form) &&
q.status === 'APPROVED', q.status === 'APPROVED',
) )
.map((q) => ({ id: q.id, title: q.title, form: q.form, points: q.points })); .map((q) => ({ id: q.id, title: q.title, form: q.form, points: q.points }));

View File

@@ -3,6 +3,13 @@ import type { EstimatedTime } from "../../question/types/question";
import type { QuestionForm } from "../../question/types/question"; import type { QuestionForm } from "../../question/types/question";
export type { QuestionForm }; export type { QuestionForm };
/**
* The paper's own form — unlike a question's (QuestionForm), an exam may mix
* both: BOTH means a mixed paper, resolved per-question against each
* question's own ESSAY/CHOICE form. Mirrors backend EExamForm.
*/
export type ExamForm = QuestionForm | "BOTH";
export type ExamType = "WRITTEN" | "ORAL"; export type ExamType = "WRITTEN" | "ORAL";
export type ExamAdministrationMethod = "OFFLINE" | "ONLINE"; export type ExamAdministrationMethod = "OFFLINE" | "ONLINE";
export type ExamEvaluationMethod = "SUM" | "AVERAGE" | "PERCENTAGE"; export type ExamEvaluationMethod = "SUM" | "AVERAGE" | "PERCENTAGE";
@@ -39,7 +46,7 @@ export interface Exam {
date: string; date: string;
givenTime: EstimatedTime | null; givenTime: EstimatedTime | null;
type: ExamType; type: ExamType;
form: QuestionForm; form: ExamForm;
venue: string; venue: string;
administrationMethod: ExamAdministrationMethod; administrationMethod: ExamAdministrationMethod;
evaluationMethod: ExamEvaluationMethod; evaluationMethod: ExamEvaluationMethod;
@@ -63,7 +70,7 @@ export interface CreateExamPayload {
date: string; date: string;
givenTime: EstimatedTime; givenTime: EstimatedTime;
type: ExamType; type: ExamType;
form: QuestionForm; form: ExamForm;
venue: string; venue: string;
administrationMethod: ExamAdministrationMethod; administrationMethod: ExamAdministrationMethod;
evaluationMethod: ExamEvaluationMethod; evaluationMethod: ExamEvaluationMethod;
@@ -79,7 +86,7 @@ export interface UpdateExamPayload {
date?: string; date?: string;
givenTime?: EstimatedTime; givenTime?: EstimatedTime;
type?: ExamType; type?: ExamType;
form?: QuestionForm; form?: ExamForm;
venue?: string; venue?: string;
administrationMethod?: ExamAdministrationMethod; administrationMethod?: ExamAdministrationMethod;
evaluationMethod?: ExamEvaluationMethod; evaluationMethod?: ExamEvaluationMethod;

View File

@@ -65,16 +65,9 @@ export interface ActionDefinition {
*/ */
export const ACTIONS: ActionDefinition[] = [ export const ACTIONS: ActionDefinition[] = [
// ------------------------------------------------------------- workflow // ------------------------------------------------------------- workflow
{ // Claim is deliberately absent here: an officer claims from the queue
id: 'claim', // (LicenseQueuePage), not from this detail page. That implementation is
tier: 'workflow', // separate — see LicenseQueuePage/actions.tsx — and is unaffected by this.
labelKey: 'review.actions.claim',
// Mirrors the CLAIM transition's `from` list: an examined cert (CoC/CoP)
// sits in ELIGIBILITY_PAID once its eligibility fee clears, not SUBMITTED.
from: ['SUBMITTED', 'ELIGIBILITY_PAID'],
permissions: ['can:claim:license-application'],
emphasis: 'light',
},
{ {
id: 'assign', id: 'assign',
tier: 'workflow', tier: 'workflow',