Hints input + preset chips + count picker; generated questions land in the existing editor blocks for the supervisor to review before publish.
184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
import type { ExamDetail, ExamQuestion, ExamResultItem, ExamSubmitResult, ExamSummary, SupervisorExamDraft } from '@/types/api';
|
|
import { apiRequest } from './api';
|
|
|
|
type RawExamQuestion = {
|
|
id: number;
|
|
questionType: string;
|
|
stem: string;
|
|
options?: string[];
|
|
correctAnswers?: string[];
|
|
score: number;
|
|
explanation?: string;
|
|
sortOrder?: number;
|
|
};
|
|
|
|
type RawExamSummary = {
|
|
id: number;
|
|
title: string;
|
|
projectCode?: string;
|
|
positionCode?: string;
|
|
status: string;
|
|
totalScore: number;
|
|
passScore: number;
|
|
dueDate?: string | null;
|
|
questionCount: number;
|
|
targetCount: number;
|
|
submissionCount: number;
|
|
bestScore?: number | null;
|
|
passed?: boolean | null;
|
|
};
|
|
|
|
type RawExamDetail = Omit<RawExamSummary, 'questionCount' | 'targetCount' | 'submissionCount' | 'bestScore' | 'passed'> & {
|
|
questions: RawExamQuestion[];
|
|
targets: ExamDetail['targets'];
|
|
};
|
|
|
|
const normalizeSummary = (item: RawExamSummary): ExamSummary => ({
|
|
...item,
|
|
passingScore: item.passScore
|
|
});
|
|
|
|
const normalizeDetail = (item: RawExamDetail): ExamDetail => ({
|
|
...item,
|
|
passingScore: item.passScore,
|
|
questionCount: item.questions?.length || 0,
|
|
targetCount: item.targets?.length || 0,
|
|
submissionCount: 0,
|
|
bestScore: null,
|
|
passed: null,
|
|
questions: (item.questions || []).map((question) => ({
|
|
id: question.id,
|
|
format: question.questionType,
|
|
stem: question.stem,
|
|
options: question.options || [],
|
|
score: question.score,
|
|
answer: question.correctAnswers || [],
|
|
explanation: question.explanation || ''
|
|
})),
|
|
targets: item.targets || []
|
|
});
|
|
|
|
const serializeDraft = (draft: SupervisorExamDraft) => ({
|
|
title: draft.title,
|
|
projectCode: draft.projectCode,
|
|
positionCode: draft.positionCode,
|
|
passScore: draft.passScore,
|
|
dueDate: draft.dueDate,
|
|
targets: draft.targets,
|
|
questions: draft.questions.map((question, index) => ({
|
|
id: question.id > 0 && question.id < 1_000_000_000 ? question.id : null,
|
|
questionType: question.format,
|
|
stem: question.stem,
|
|
options: question.options,
|
|
correctAnswers: Array.isArray(question.answer) ? question.answer : [String(question.answer || '')],
|
|
score: question.score,
|
|
explanation: question.explanation || '',
|
|
sortOrder: index + 1
|
|
}))
|
|
});
|
|
|
|
export const listMyExams = async () =>
|
|
(await apiRequest<RawExamSummary[]>({
|
|
url: '/api/aihr/mobile/exams',
|
|
method: 'GET',
|
|
timeout: 15000
|
|
})).map(normalizeSummary);
|
|
|
|
export const getMyExam = async (id: number) =>
|
|
normalizeDetail(await apiRequest<RawExamDetail>({
|
|
url: `/api/aihr/mobile/exams/${id}`,
|
|
method: 'GET',
|
|
timeout: 15000
|
|
}));
|
|
|
|
export const submitMyExam = (id: number, answers: Record<string, string | string[]>, requestId: string) =>
|
|
apiRequest<ExamSubmitResult>({
|
|
url: `/api/aihr/mobile/exams/${id}/submit`,
|
|
method: 'POST',
|
|
data: {
|
|
requestId,
|
|
answers: Object.entries(answers).map(([questionId, value]) => ({
|
|
questionId: Number(questionId),
|
|
answers: Array.isArray(value) ? value : [String(value)]
|
|
}))
|
|
},
|
|
timeout: 30000
|
|
});
|
|
|
|
export const listSupervisorExams = async () =>
|
|
(await apiRequest<RawExamSummary[]>({
|
|
url: '/api/aihr/mobile/exams/supervisor',
|
|
method: 'GET',
|
|
timeout: 15000
|
|
})).map(normalizeSummary);
|
|
|
|
export const getSupervisorExam = async (id: number) =>
|
|
normalizeDetail(await apiRequest<RawExamDetail>({
|
|
url: `/api/aihr/mobile/exams/supervisor/${id}`,
|
|
method: 'GET',
|
|
timeout: 15000
|
|
}));
|
|
|
|
export const saveSupervisorExam = async (draft: SupervisorExamDraft) =>
|
|
normalizeDetail(await apiRequest<RawExamDetail>({
|
|
url: '/api/aihr/mobile/exams/supervisor',
|
|
method: 'POST',
|
|
data: serializeDraft(draft),
|
|
timeout: 30000
|
|
}));
|
|
|
|
export const updateSupervisorExam = async (id: number, draft: SupervisorExamDraft) =>
|
|
normalizeDetail(await apiRequest<RawExamDetail>({
|
|
url: `/api/aihr/mobile/exams/supervisor/${id}`,
|
|
method: 'PUT',
|
|
data: serializeDraft(draft),
|
|
timeout: 30000
|
|
}));
|
|
|
|
export const publishSupervisorExam = async (id: number, requestId: string) =>
|
|
normalizeDetail(await apiRequest<RawExamDetail>({
|
|
url: `/api/aihr/mobile/exams/supervisor/${id}/publish`,
|
|
method: 'POST',
|
|
data: { requestId },
|
|
timeout: 30000
|
|
}));
|
|
|
|
export const getSupervisorExamResults = (id: number) =>
|
|
apiRequest<ExamResultItem[]>({
|
|
url: `/api/aihr/mobile/exams/supervisor/${id}/results`,
|
|
method: 'GET',
|
|
timeout: 15000
|
|
});
|
|
|
|
|
|
type RawAiGenerateResponse = {
|
|
questions: RawExamQuestion[];
|
|
groundedFragments?: number;
|
|
};
|
|
|
|
export const generateSupervisorExamQuestions = async (data: {
|
|
positionCode: string;
|
|
hints: string;
|
|
count: number;
|
|
formats: string[];
|
|
}): Promise<{ questions: ExamQuestion[]; groundedFragments: number }> => {
|
|
const response = await apiRequest<RawAiGenerateResponse>({
|
|
url: '/api/aihr/mobile/exams/supervisor/ai-generate',
|
|
method: 'POST',
|
|
data,
|
|
timeout: 60000
|
|
});
|
|
return {
|
|
groundedFragments: response.groundedFragments || 0,
|
|
questions: (response.questions || []).map((question, index) => ({
|
|
id: Date.now() + index,
|
|
format: question.questionType,
|
|
stem: question.stem,
|
|
options: question.options || [],
|
|
score: question.score,
|
|
answer: question.questionType === 'multiple' ? (question.correctAnswers || []) : (question.correctAnswers?.[0] || ''),
|
|
explanation: question.explanation || ''
|
|
}))
|
|
};
|
|
};
|