This commit is contained in:
2026-06-30 11:51:34 +07:00
parent fd08bee7ab
commit 0abf48ea2d
28 changed files with 3514 additions and 37 deletions

View File

@@ -4,7 +4,7 @@ function getToken(): string | null {
return localStorage.getItem('sc_staff_token');
}
async function staffFetch(path: string, init: RequestInit = {}): Promise<Response> {
export async function staffFetch(path: string, init: RequestInit = {}): Promise<Response> {
const headers = new Headers(init.headers);
const token = getToken();
if (token) headers.set('Authorization', `Bearer ${token}`);
@@ -16,7 +16,10 @@ async function staffFetch(path: string, init: RequestInit = {}): Promise<Respons
async function parseError(res: Response, fallback: string): Promise<never> {
const errData = await res.json().catch(() => ({}));
throw new Error((errData as { error?: string }).error || fallback);
const msg = (errData as { error?: string }).error;
if (msg) throw new Error(msg);
if (res.status === 413) throw new Error('File quá lớn (tối đa 100MB)');
throw new Error(`${fallback} (HTTP ${res.status})`);
}
export interface StaffUser {
@@ -568,3 +571,185 @@ export const apiPushAttendanceQLDT = async (rkId: number, date: string, period:
}
return res.json();
};
async function staffUpload(path: string, form: FormData): Promise<Response> {
const headers = new Headers();
const token = getToken();
if (token) headers.set('Authorization', `Bearer ${token}`);
return fetch(`${API_BASE}${path}`, { method: 'POST', headers, body: form });
}
export interface ExamRoomItem {
id: number;
name: string;
startTime: string;
endTime: string;
allowedApps: string;
quizUrl: string;
status: 'draft' | 'ready' | 'ended' | 'cancelled';
studentCount: number;
paperCount: number;
displayStatus: 'draft' | 'ready' | 'active' | 'ended' | 'cancelled';
}
export interface ExamPaperResource {
id: number;
examPaperId: number;
fileName: string;
}
export interface ExamPaper {
id: number;
examRoomId: number;
title: string;
pdfPath: string;
sortOrder: number;
resources: ExamPaperResource[];
}
export interface ExamRoomStudent {
id: number;
examRoomId: number;
studentRkId: number;
assignedPaperId?: number;
paperSentAt?: string;
paperScheduledAt?: string;
fullName: string;
studentCode: string;
email: string;
avatar?: string;
paperTitle?: string;
submitted: boolean;
}
export interface ExamSubmission {
id: number;
examRoomId: number;
studentRkId: number;
fileName: string;
createdAt: string;
fullName: string;
studentCode: string;
}
export const apiExam = {
list: async (): Promise<{ data: ExamRoomItem[] }> => {
const res = await staffFetch('/exam-rooms');
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
create: async (payload: { name: string; startTime: string; endTime: string; allowedApps?: string; quizUrl?: string }) => {
const res = await staffFetch('/exam-rooms', { method: 'POST', body: JSON.stringify(payload) });
if (!res.ok) await parseError(res, 'Tạo phòng thi thất bại');
return res.json();
},
get: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}`);
if (!res.ok) await parseError(res, 'Không tìm thấy');
return res.json() as Promise<{
room: ExamRoomItem;
papers: ExamPaper[];
students: ExamRoomStudent[];
displayStatus: string;
editable: boolean;
prepEditable: boolean;
canPublish: boolean;
canUnpublish: boolean;
canCancel: boolean;
}>;
},
publish: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}/publish`, { method: 'POST' });
if (!res.ok) {
const data = await res.json().catch(() => ({})) as { error?: string; issues?: string[] };
if (data.issues?.length) {
throw new Error(data.issues.join('\n'));
}
throw new Error(data.error || 'Đẩy phòng thi thất bại');
}
return res.json();
},
unpublish: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}/unpublish`, { method: 'POST' });
if (!res.ok) await parseError(res, 'Thu hồi thất bại');
return res.json();
},
cancel: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}/cancel`, { method: 'POST' });
if (!res.ok) await parseError(res, 'Hủy phòng thi thất bại');
return res.json();
},
update: async (id: number, payload: Partial<{ name: string; startTime: string; endTime: string; allowedApps: string; quizUrl: string }>) => {
const res = await staffFetch(`/exam-rooms/${id}`, { method: 'PATCH', body: JSON.stringify(payload) });
if (!res.ok) await parseError(res, 'Cập nhật thất bại');
return res.json();
},
remove: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}`, { method: 'DELETE' });
if (!res.ok) await parseError(res, 'Xóa thất bại');
return res.json();
},
searchStudents: async (q: string) => {
const res = await staffFetch(`/exam-rooms/search-students?q=${encodeURIComponent(q)}`);
if (!res.ok) await parseError(res, 'Tìm kiếm thất bại');
return res.json() as Promise<{ data: { studentRkId: number; fullName: string; studentCode: string; email: string; classCodes: string }[] }>;
},
addStudents: async (id: number, studentRkIds: number[]) => {
const res = await staffFetch(`/exam-rooms/${id}/students`, { method: 'POST', body: JSON.stringify({ studentRkIds }) });
if (!res.ok) await parseError(res, 'Thêm sinh viên thất bại');
return res.json();
},
removeStudent: async (id: number, studentRkId: number) => {
const res = await staffFetch(`/exam-rooms/${id}/students/${studentRkId}`, { method: 'DELETE' });
if (!res.ok) await parseError(res, 'Xóa sinh viên thất bại');
return res.json();
},
fetchOnlineStudents: async (id: number): Promise<{ onlineStudentIds: number[] }> => {
const res = await staffFetch(`/exam-rooms/${id}/online-students`);
if (!res.ok) throw new Error('Không tải được trạng thái online');
return res.json();
},
uploadPackage: async (id: number, title: string, pdf: File, resources: File[] = []) => {
const form = new FormData();
form.append('title', title);
form.append('pdf', pdf);
resources.forEach((f) => form.append('resources', f));
const res = await staffUpload(`/exam-rooms/${id}/papers`, form);
if (!res.ok) await parseError(res, 'Tạo gói đề thất bại');
return res.json();
},
uploadPaper: async (id: number, title: string, pdf: File, resources: File[] = []) => {
return apiExam.uploadPackage(id, title, pdf, resources);
},
uploadPackageResources: async (id: number, paperId: number, files: File[]) => {
const form = new FormData();
files.forEach((f) => form.append('resources', f));
const res = await staffUpload(`/exam-rooms/${id}/papers/${paperId}/resources`, form);
if (!res.ok) await parseError(res, 'Thêm tài nguyên thất bại');
return res.json();
},
uploadResource: async (id: number, paperId: number, file: File) => {
return apiExam.uploadPackageResources(id, paperId, [file]);
},
deletePaper: async (id: number, paperId: number) => {
const res = await staffFetch(`/exam-rooms/${id}/papers/${paperId}`, { method: 'DELETE' });
if (!res.ok) await parseError(res, 'Xóa gói đề thất bại');
return res.json();
},
assignRandom: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}/assign-random`, { method: 'POST' });
if (!res.ok) await parseError(res, 'Chia gói đề thất bại');
return res.json() as Promise<{ assigned: number; packages: number }>;
},
sendPapers: async (id: number, payload?: { scheduledAt?: string; studentRkIds?: number[] }) => {
const res = await staffFetch(`/exam-rooms/${id}/send-papers`, { method: 'POST', body: JSON.stringify(payload || {}) });
if (!res.ok) await parseError(res, 'Gửi gói đề thất bại');
return res.json();
},
listSubmissions: async (id: number) => {
const res = await staffFetch(`/exam-rooms/${id}/submissions`);
if (!res.ok) await parseError(res, 'Failed');
return res.json() as Promise<{ data: ExamSubmission[] }>;
},
downloadSubmission: (id: number, subId: number) => `${API_BASE}/exam-rooms/${id}/submissions/${subId}/download`,
};