tam chat
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user