add
This commit is contained in:
@@ -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`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user