This commit is contained in:
2026-06-30 10:39:29 +07:00
parent aaafa6ada9
commit d9c8e3930b
28 changed files with 3040 additions and 167 deletions

View File

@@ -1,5 +1,161 @@
const API_BASE = 'http://127.0.0.1:8080/api';
function getToken(): string | null {
return localStorage.getItem('sc_staff_token');
}
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}`);
if (init.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
return fetch(`${API_BASE}${path}`, { ...init, headers });
}
async function parseError(res: Response, fallback: string): Promise<never> {
const errData = await res.json().catch(() => ({}));
throw new Error((errData as { error?: string }).error || fallback);
}
export interface StaffUser {
id: number;
email: string;
fullName: string;
mustChangePassword: boolean;
}
export const apiAuth = {
provision: async (email: string): Promise<{ ok: boolean; message: string }> => {
const res = await fetch(`${API_BASE}/auth/provision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!res.ok) await parseError(res, 'Provision failed');
return res.json();
},
login: async (email: string, password: string): Promise<{ token: string; staff: StaffUser; mustChangePassword: boolean }> => {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) await parseError(res, 'Đăng nhập thất bại');
return res.json();
},
me: async (): Promise<{ staff: StaffUser }> => {
const res = await staffFetch('/auth/me');
if (!res.ok) await parseError(res, 'Unauthorized');
return res.json();
},
changePassword: async (oldPassword: string, newPassword: string): Promise<{ ok: boolean; token: string; staff: StaffUser }> => {
const res = await staffFetch('/auth/change-password', {
method: 'POST',
body: JSON.stringify({ oldPassword, newPassword }),
});
if (!res.ok) await parseError(res, 'Đổi mật khẩu thất bại');
return res.json();
},
forgotPassword: async (email: string): Promise<{ ok: boolean; message: string }> => {
const res = await fetch(`${API_BASE}/auth/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!res.ok) await parseError(res, 'Gửi mail thất bại');
return res.json();
},
resetPassword: async (token: string, newPassword: string): Promise<{ ok: boolean; message: string }> => {
const res = await fetch(`${API_BASE}/auth/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, newPassword }),
});
if (!res.ok) await parseError(res, 'Đặt lại mật khẩu thất bại');
return res.json();
},
};
export interface EmailDomainItem {
id: number;
domain: string;
isActive: boolean;
}
export const apiAdmin = {
listEmailDomains: async (): Promise<{ data: EmailDomainItem[] }> => {
const res = await staffFetch('/admin/email-domains');
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
addEmailDomain: async (domain: string) => {
const res = await staffFetch('/admin/email-domains', { method: 'POST', body: JSON.stringify({ domain }) });
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
deleteEmailDomain: async (id: number) => {
const res = await staffFetch(`/admin/email-domains/${id}`, { method: 'DELETE' });
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
};
export interface ChatStudent {
studentRkId: number;
fullName: string;
studentCode: string;
email: string;
online: boolean;
}
export interface ChatMessage {
id: number;
staffId: number;
studentRkId: number;
senderRole: 'staff' | 'student';
body: string;
createdAt: string;
staffName?: string;
}
export interface ChatConversation {
studentRkId: number;
fullName: string;
studentCode: string;
lastMessage: string;
lastAt: string;
unread: number;
}
export const apiChat = {
listConversations: async (): Promise<{ data: ChatConversation[] }> => {
const res = await staffFetch('/chat/conversations');
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
searchStudents: async (q = ''): Promise<{ data: ChatStudent[] }> => {
const params = q ? `?q=${encodeURIComponent(q)}` : '';
const res = await staffFetch(`/chat/students${params}`);
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
listMessages: async (studentRkId: number): Promise<{ data: ChatMessage[] }> => {
const res = await staffFetch(`/chat/messages/${studentRkId}`);
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
sendMessage: async (studentRkId: number, body: string): Promise<{ ok: boolean; data: ChatMessage }> => {
const res = await staffFetch('/chat/messages', {
method: 'POST',
body: JSON.stringify({ studentRkId, body }),
});
if (!res.ok) await parseError(res, 'Failed');
return res.json();
},
};
export interface ClassItem {
rkId: number;
name: string;
@@ -125,7 +281,7 @@ export interface StudentSessionLogItem {
export const api = {
getStats: async (): Promise<StatsResponse> => {
const res = await fetch(`${API_BASE}/stats`);
const res = await staffFetch('/stats');
if (!res.ok) throw new Error('Failed to fetch dashboard stats');
return res.json();
},
@@ -145,15 +301,14 @@ export const api = {
if (params.systemRkId) query.set('systemRkId', String(params.systemRkId));
if (params.studyingOnly) query.set('studyingOnly', 'true');
const res = await fetch(`${API_BASE}/classes?${query}`);
const res = await staffFetch(`/classes?${query}`);
if (!res.ok) throw new Error('Failed to fetch classes');
return res.json();
},
updateClassStudying: async (rkId: number, isStudying: boolean): Promise<any> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/studying`, {
const res = await staffFetch(`/classes/${rkId}/studying`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isStudying }),
});
if (!res.ok) {
@@ -164,11 +319,17 @@ export const api = {
},
getClassStudents: async (rkId: number): Promise<{ data: StudentItem[]; total: number }> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/students`);
const res = await staffFetch(`/classes/${rkId}/students`);
if (!res.ok) throw new Error('Failed to fetch class students roster');
return res.json();
},
getClass: async (rkId: number): Promise<ClassItem> => {
const res = await staffFetch(`/classes/${rkId}`);
if (!res.ok) throw new Error('Failed to fetch class');
return res.json();
},
getStudents: async (params: {
page: number;
pageSize: number;
@@ -180,13 +341,13 @@ export const api = {
});
if (params.q) query.set('q', params.q);
const res = await fetch(`${API_BASE}/students?${query}`);
const res = await staffFetch(`/students?${query}`);
if (!res.ok) throw new Error('Failed to fetch students');
return res.json();
},
startClassesSync: async (): Promise<{ ok: boolean; message: string }> => {
const res = await fetch(`${API_BASE}/sync/classes/start`, { method: 'POST' });
const res = await staffFetch('/sync/classes/start', { method: 'POST' });
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
throw new Error(errData.error || 'Failed to start classes sync');
@@ -195,13 +356,13 @@ export const api = {
},
getClassesSyncStatus: async (): Promise<SyncStatus> => {
const res = await fetch(`${API_BASE}/sync/classes/status`);
const res = await staffFetch('/sync/classes/status');
if (!res.ok) throw new Error('Failed to get classes sync status');
return res.json();
},
startStudentsSync: async (): Promise<{ ok: boolean; message: string }> => {
const res = await fetch(`${API_BASE}/sync/students/start`, { method: 'POST' });
const res = await staffFetch('/sync/students/start', { method: 'POST' });
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
throw new Error(errData.error || 'Failed to start students sync');
@@ -210,7 +371,7 @@ export const api = {
},
getStudentsSyncStatus: async (): Promise<SyncStatus> => {
const res = await fetch(`${API_BASE}/sync/students/status`);
const res = await staffFetch('/sync/students/status');
if (!res.ok) throw new Error('Failed to get students sync status');
return res.json();
},
@@ -218,21 +379,20 @@ export const api = {
// Export individual learning functions to simplify imports in components
export const apiFetchActiveSchedules = async (): Promise<{ data: any[] }> => {
const res = await fetch(`${API_BASE}/classes/schedules`);
const res = await staffFetch('/classes/schedules');
if (!res.ok) throw new Error('Failed to fetch active schedules');
return res.json();
};
export const apiFetchClassSchedule = async (rkId: number): Promise<{ data: ClassScheduleItem[] }> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/schedule`);
const res = await staffFetch(`/classes/${rkId}/schedule`);
if (!res.ok) throw new Error('Failed to fetch class schedule');
return res.json();
};
export const apiSaveClassSchedule = async (rkId: number, schedules: ClassScheduleItem[]): Promise<any> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/schedule`, {
const res = await staffFetch(`/classes/${rkId}/schedule`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ schedules }),
});
if (!res.ok) throw new Error('Failed to save class schedule');
@@ -240,21 +400,20 @@ export const apiSaveClassSchedule = async (rkId: number, schedules: ClassSchedul
};
export const apiDeleteClassSchedule = async (rkId: number): Promise<any> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/schedule`, { method: 'DELETE' });
const res = await staffFetch(`/classes/${rkId}/schedule`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete class schedule');
return res.json();
};
export const apiFetchAllowedApps = async (rkId: number): Promise<{ classRkId: number; keywords: string }> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/allowed-apps`);
const res = await staffFetch(`/classes/${rkId}/allowed-apps`);
if (!res.ok) throw new Error('Failed to fetch allowed apps keywords');
return res.json();
};
export const apiSaveAllowedApps = async (rkId: number, keywords: string): Promise<any> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/allowed-apps`, {
const res = await staffFetch(`/classes/${rkId}/allowed-apps`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keywords }),
});
if (!res.ok) throw new Error('Failed to save allowed apps keywords');
@@ -277,7 +436,7 @@ export const apiFetchAppPool = async (q = '', limit = 50): Promise<{ data: AppPo
if (q.trim()) params.set('q', q.trim());
params.set('limit', String(limit));
const query = params.toString() ? `?${params.toString()}` : '';
const res = await fetch(`${API_BASE}/app-pool${query}`);
const res = await staffFetch(`/app-pool${query}`);
if (!res.ok) throw new Error('Failed to fetch app pool');
return res.json();
};
@@ -305,21 +464,20 @@ export const apiFetchWifiPool = async (q = '', limit = 50): Promise<{ data: Wifi
if (q.trim()) params.set('q', q.trim());
params.set('limit', String(limit));
const query = params.toString() ? `?${params.toString()}` : '';
const res = await fetch(`${API_BASE}/wifi-pool${query}`);
const res = await staffFetch(`/wifi-pool${query}`);
if (!res.ok) throw new Error('Failed to fetch wifi pool');
return res.json();
};
export const apiFetchAcceptedWifis = async (): Promise<{ data: AcceptedWifiItem[] }> => {
const res = await fetch(`${API_BASE}/network/accepted-wifis`);
const res = await staffFetch('/network/accepted-wifis');
if (!res.ok) throw new Error('Failed to fetch accepted wifis');
return res.json();
};
export const apiSaveAcceptedWifis = async (items: WifiAcceptItem[]): Promise<any> => {
const res = await fetch(`${API_BASE}/network/accepted-wifis`, {
const res = await staffFetch('/network/accepted-wifis', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
if (!res.ok) throw new Error('Failed to save accepted wifis');
@@ -338,13 +496,13 @@ export const apiFetchClassSessionLogs = async (
if (date) params.set('date', date);
if (period) params.set('period', String(period));
const query = params.toString() ? `?${params.toString()}` : '';
const res = await fetch(`${API_BASE}/classes/${rkId}/session-logs${query}`);
const res = await staffFetch(`/classes/${rkId}/session-logs${query}`);
if (!res.ok) throw new Error('Failed to fetch class session logs');
return res.json();
};
export const apiFetchOnlineStudents = async (rkId: number): Promise<{ onlineStudentIds: number[] }> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/online-students`);
const res = await staffFetch(`/classes/${rkId}/online-students`);
if (!res.ok) throw new Error('Failed to fetch online students list');
return res.json();
};
@@ -356,7 +514,7 @@ export const apiFetchClassCourses = async (rkId: number): Promise<{
warning?: string;
hint?: string;
}> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/courses`);
const res = await staffFetch(`/classes/${rkId}/courses`);
const body = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(body.error || body.hint || 'Không tải được danh sách môn học từ QLĐT');
@@ -365,20 +523,20 @@ export const apiFetchClassCourses = async (rkId: number): Promise<{
};
export const apiApplyScheduleTemplate = async (rkId: number): Promise<{ created: number; skipped: number }> => {
const res = await fetch(`${API_BASE}/classes/${rkId}/schedule/apply-template`, { method: 'POST' });
const res = await staffFetch(`/classes/${rkId}/schedule/apply-template`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to apply schedule template');
return res.json();
};
export const apiFetchAttendanceShifts = async (rkId: number, date?: string) => {
const q = date ? `?date=${date}` : '';
const res = await fetch(`${API_BASE}/classes/${rkId}/attendance/shifts${q}`);
const res = await staffFetch(`/classes/${rkId}/attendance/shifts${q}`);
if (!res.ok) throw new Error('Failed to fetch attendance shifts');
return res.json();
};
export const apiFetchAttendance = async (rkId: number, date: string, period: number) => {
const res = await fetch(`${API_BASE}/classes/${rkId}/attendance?date=${date}&period=${period}`);
const res = await staffFetch(`/classes/${rkId}/attendance?date=${date}&period=${period}`);
if (!res.ok) throw new Error('Failed to fetch attendance');
return res.json();
};
@@ -387,9 +545,8 @@ export const apiUpdateAttendanceStatus = async (
rkId: number,
payload: { date: string; period: number; studentRkId: number; status: number }
) => {
const res = await fetch(`${API_BASE}/classes/${rkId}/attendance/status`, {
const res = await staffFetch(`/classes/${rkId}/attendance/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
@@ -400,9 +557,8 @@ export const apiUpdateAttendanceStatus = async (
};
export const apiPushAttendanceQLDT = async (rkId: number, date: string, period: number) => {
const res = await fetch(`${API_BASE}/classes/${rkId}/attendance/push-qldt`, {
const res = await staffFetch(`/classes/${rkId}/attendance/push-qldt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date, period }),
});
if (!res.ok) {