Spaces:
Running
Running
| // ... existing imports | |
| import { User, ClassInfo, SystemConfig, Subject, School, Schedule, GameSession, StudentReward, LuckyDrawConfig, Attendance, LeaveRequest, AchievementConfig, SchoolCalendarEntry, TeacherExchangeConfig, Wish, Feedback, AIChatMessage, Todo } from '../types'; | |
| // ... existing getBaseUrl ... | |
| const getBaseUrl = () => { | |
| let isProd = false; | |
| try { | |
| // @ts-ignore | |
| if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.PROD) { | |
| isProd = true; | |
| } | |
| } catch (e) {} | |
| if (isProd || (typeof window !== 'undefined' && window.location.port === '7860')) { | |
| return '/api'; | |
| } | |
| return 'http://localhost:7860/api'; | |
| }; | |
| const API_BASE_URL = getBaseUrl(); | |
| async function request(endpoint: string, options: RequestInit = {}) { | |
| const headers: any = { 'Content-Type': 'application/json', ...options.headers }; | |
| if (typeof window !== 'undefined') { | |
| const currentUser = JSON.parse(localStorage.getItem('user') || 'null'); | |
| const selectedSchoolId = localStorage.getItem('admin_view_school_id'); | |
| if (currentUser?.role === 'ADMIN' && selectedSchoolId) { | |
| headers['x-school-id'] = selectedSchoolId; | |
| } else if (currentUser?.schoolId) { | |
| headers['x-school-id'] = currentUser.schoolId; | |
| } | |
| if (currentUser?.role) { | |
| headers['x-user-role'] = currentUser.role; | |
| headers['x-user-username'] = currentUser.username; | |
| } | |
| } | |
| const res = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers }); | |
| if (!res.ok) { | |
| if (res.status === 401) throw new Error('AUTH_FAILED'); | |
| const errorData = await res.json().catch(() => ({})); | |
| const errorMessage = errorData.error || errorData.message || `Server Error: ${res.status}`; | |
| if (errorData.error === 'PENDING_APPROVAL') throw new Error('PENDING_APPROVAL'); | |
| if (errorData.error === 'BANNED') throw new Error('BANNED'); | |
| if (errorData.error === 'CONFLICT') throw new Error(errorData.message); | |
| if (errorData.error === 'INVALID_PASSWORD') throw new Error('INVALID_PASSWORD'); | |
| throw new Error(errorMessage); | |
| } | |
| return res.json(); | |
| } | |
| export const api = { | |
| init: () => console.log('🔗 API:', API_BASE_URL), | |
| auth: { | |
| login: async (username: string, password: string): Promise<User> => { | |
| const user = await request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); | |
| if (typeof window !== 'undefined') { | |
| localStorage.setItem('user', JSON.stringify(user)); | |
| localStorage.removeItem('admin_view_school_id'); | |
| } | |
| return user; | |
| }, | |
| refreshSession: async (): Promise<User | null> => { | |
| try { | |
| const user = await request('/auth/me'); | |
| if (typeof window !== 'undefined' && user) { | |
| localStorage.setItem('user', JSON.stringify(user)); | |
| } | |
| return user; | |
| } catch (e) { return null; } | |
| }, | |
| register: async (data: any): Promise<User> => { | |
| return await request('/auth/register', { method: 'POST', body: JSON.stringify(data) }); | |
| }, | |
| updateProfile: async (data: any): Promise<any> => { | |
| return await request('/auth/update-profile', { method: 'POST', body: JSON.stringify(data) }); | |
| }, | |
| logout: () => { | |
| if (typeof window !== 'undefined') { | |
| localStorage.removeItem('user'); | |
| localStorage.removeItem('admin_view_school_id'); | |
| } | |
| }, | |
| getCurrentUser: (): User | null => { | |
| if (typeof window !== 'undefined') { | |
| try { | |
| const stored = localStorage.getItem('user'); | |
| if (stored) return JSON.parse(stored); | |
| } catch (e) { | |
| localStorage.removeItem('user'); | |
| } | |
| } | |
| return null; | |
| } | |
| }, | |
| schools: { | |
| getPublic: () => request('/public/schools'), | |
| getAll: () => request('/schools'), | |
| add: (data: School) => request('/schools', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string, data: Partial<School>) => request(`/schools/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string) => request(`/schools/${id}`, { method: 'DELETE' }) | |
| }, | |
| users: { | |
| getAll: (options?: { global?: boolean; role?: string }) => { | |
| const params = new URLSearchParams(); | |
| if (options?.global) params.append('global', 'true'); | |
| if (options?.role) params.append('role', options.role); | |
| return request(`/users?${params.toString()}`); | |
| }, | |
| update: (id: string, data: Partial<User>) => request(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string) => request(`/users/${id}`, { method: 'DELETE' }), | |
| applyClass: (data: { userId: string, type: 'CLAIM'|'RESIGN', targetClass?: string, action: 'APPLY'|'APPROVE'|'REJECT' }) => | |
| request('/users/class-application', { method: 'POST', body: JSON.stringify(data) }), | |
| getTeachersForClass: (className: string) => request(`/classes/${encodeURIComponent(className)}/teachers`), | |
| saveMenuOrder: (userId: string, order: string[]) => request(`/users/${userId}/menu-order`, { method: 'PUT', body: JSON.stringify({ menuOrder: order }) }), // NEW | |
| }, | |
| students: { | |
| getAll: () => request('/students'), | |
| add: (data: any) => request('/students', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string, data: any) => request(`/students/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string | number) => request(`/students/${id}`, { method: 'DELETE' }), | |
| promote: (data: { teacherFollows: boolean }) => request('/students/promote', { method: 'POST', body: JSON.stringify(data) }), | |
| transfer: (data: { studentId: string, targetClass: string }) => request('/students/transfer', { method: 'POST', body: JSON.stringify(data) }) | |
| }, | |
| classes: { | |
| getAll: () => request('/classes'), | |
| add: (data: ClassInfo) => request('/classes', { method: 'POST', body: JSON.stringify(data) }), | |
| delete: (id: string | number) => request(`/classes/${id}`, { method: 'DELETE' }) | |
| }, | |
| subjects: { | |
| getAll: () => request('/subjects'), | |
| add: (data: Subject) => request('/subjects', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string | number, data: Partial<Subject>) => request(`/subjects/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string | number) => request(`/subjects/${id}`, { method: 'DELETE' }) | |
| }, | |
| exams: { | |
| getAll: () => request('/exams'), | |
| save: (data: any) => request('/exams', { method: 'POST', body: JSON.stringify(data) }) | |
| }, | |
| courses: { | |
| getAll: () => request('/courses'), | |
| add: (data: any) => request('/courses', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string | number, data: any) => request(`/courses/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string | number) => request(`/courses/${id}`, { method: 'DELETE' }) | |
| }, | |
| scores: { | |
| getAll: () => request('/scores'), | |
| add: (data: any) => request('/scores', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string | number, data: any) => request(`/scores/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string | number) => request(`/scores/${id}`, { method: 'DELETE' }) | |
| }, | |
| schedules: { | |
| get: (params: { className?: string; teacherName?: string; grade?: string }) => { | |
| const qs = new URLSearchParams(params as any).toString(); | |
| return request(`/schedules?${qs}`); | |
| }, | |
| save: (data: Schedule) => request('/schedules', { method: 'POST', body: JSON.stringify(data) }), | |
| delete: (params: { className: string; dayOfWeek: number; period: number }) => { | |
| const qs = new URLSearchParams(params as any).toString(); | |
| return request(`/schedules?${qs}`, { method: 'DELETE' }); | |
| } | |
| }, | |
| attendance: { | |
| checkIn: (data: { studentId: string, date: string, status?: string }) => request('/attendance/check-in', { method: 'POST', body: JSON.stringify(data) }), | |
| get: (params: { className?: string, date?: string, studentId?: string }) => { | |
| const qs = new URLSearchParams(params as any).toString(); | |
| return request(`/attendance?${qs}`); | |
| }, | |
| batch: (data: { className: string, date: string, status?: string }) => request('/attendance/batch', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (data: { studentId: string, date: string, status: string }) => request('/attendance/update', { method: 'PUT', body: JSON.stringify(data) }), | |
| applyLeave: (data: { studentId: string, studentName: string, className: string, reason: string, startDate: string, endDate: string }) => request('/leave', { method: 'POST', body: JSON.stringify(data) }), | |
| }, | |
| calendar: { | |
| get: (className: string) => request(`/attendance/calendar?className=${className}`), | |
| add: (data: SchoolCalendarEntry) => request('/attendance/calendar', { method: 'POST', body: JSON.stringify(data) }), | |
| delete: (id: string) => request(`/attendance/calendar/${id}`, { method: 'DELETE' }) | |
| }, | |
| stats: { | |
| getSummary: () => request('/stats') | |
| }, | |
| config: { | |
| get: () => request('/config'), | |
| getPublic: () => request('/public/config'), | |
| save: (data: SystemConfig) => request('/config', { method: 'POST', body: JSON.stringify(data) }) | |
| }, | |
| notifications: { | |
| getAll: (userId: string, role: string) => request(`/notifications?userId=${userId}&role=${role}`), | |
| }, | |
| games: { | |
| getMountainSession: (className: string) => request(`/games/mountain?className=${className}`), | |
| saveMountainSession: (data: GameSession) => request('/games/mountain', { method: 'POST', body: JSON.stringify(data) }), | |
| getLuckyConfig: (className?: string, ownerId?: string) => request(`/games/lucky-config?className=${className || ''}${ownerId ? `&ownerId=${ownerId}` : ''}`), | |
| saveLuckyConfig: (data: LuckyDrawConfig) => request('/games/lucky-config', { method: 'POST', body: JSON.stringify(data) }), | |
| drawLucky: (studentId: string) => request('/games/lucky-draw', { method: 'POST', body: JSON.stringify({ studentId }) }), | |
| grantReward: (data: { studentId: string, count: number, rewardType: string, name?: string }) => request('/games/grant-reward', { method: 'POST', body: JSON.stringify(data) }), | |
| getMonsterConfig: (className: string) => request(`/games/monster-config?className=${className}`), | |
| saveMonsterConfig: (data: any) => request('/games/monster-config', { method: 'POST', body: JSON.stringify(data) }), | |
| getZenConfig: (className: string) => request(`/games/zen-config?className=${className}`), | |
| saveZenConfig: (data: any) => request('/games/zen-config', { method: 'POST', body: JSON.stringify(data) }), | |
| }, | |
| achievements: { | |
| getConfig: (className: string) => request(`/achievements/config?className=${className}`), | |
| saveConfig: (data: AchievementConfig) => request('/achievements/config', { method: 'POST', body: JSON.stringify(data) }), | |
| getStudentAchievements: (studentId: string, semester?: string) => request(`/achievements/student?studentId=${studentId}${semester ? `&semester=${semester}` : ''}`), | |
| getClassHistory: (studentIds: string[]) => request(`/achievements/student?studentIds=${studentIds.join(',')}&semester=ALL`), // NEW | |
| grant: (data: { studentId: string, achievementId: string, semester: string }) => request('/achievements/grant', { method: 'POST', body: JSON.stringify(data) }), | |
| deleteRecord: (id: string) => request(`/achievements/record/${id}`, { method: 'DELETE' }), // NEW | |
| exchange: (data: { studentId: string, ruleId: string, teacherId?: string }) => request('/achievements/exchange', { method: 'POST', body: JSON.stringify(data) }), | |
| getMyRules: () => request('/achievements/teacher-rules'), | |
| saveMyRules: (data: TeacherExchangeConfig) => request('/achievements/teacher-rules', { method: 'POST', body: JSON.stringify(data) }), | |
| getRulesByTeachers: (teacherIds: string[]) => request(`/achievements/teacher-rules?teacherIds=${teacherIds.join(',')}`), | |
| }, | |
| rewards: { | |
| getMyRewards: (studentId: string, page = 1, limit = 20) => request(`/rewards?studentId=${studentId}&page=${page}&limit=${limit}&excludeType=CONSOLATION`), | |
| getClassRewards: (page = 1, limit = 20, className?: string) => { | |
| let qs = `scope=class&page=${page}&limit=${limit}&excludeType=CONSOLATION`; | |
| if (className) qs += `&className=${className}`; | |
| return request(`/rewards?${qs}`); | |
| }, | |
| addReward: (data: Partial<StudentReward>) => request('/rewards', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string, data: Partial<StudentReward>) => request(`/rewards/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string) => request(`/rewards/${id}`, { method: 'DELETE' }), | |
| redeem: (id: string) => request(`/rewards/${id}/redeem`, { method: 'POST' }), | |
| }, | |
| batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => { | |
| return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) }); | |
| }, | |
| wishes: { | |
| getAll: (params: { teacherId?: string, studentId?: string, status?: string }) => { | |
| const qs = new URLSearchParams(params as any).toString(); | |
| return request(`/wishes?${qs}`); | |
| }, | |
| create: (data: Partial<Wish>) => request('/wishes', { method: 'POST', body: JSON.stringify(data) }), | |
| fulfill: (id: string) => request(`/wishes/${id}/fulfill`, { method: 'POST' }), | |
| randomFulfill: (teacherId: string) => request('/wishes/random-fulfill', { method: 'POST', body: JSON.stringify({ teacherId }) }), | |
| }, | |
| feedback: { | |
| getAll: (params: { targetId?: string, creatorId?: string, type?: string, status?: string }) => { | |
| const qs = new URLSearchParams(params as any).toString(); | |
| return request(`/feedback?${qs}`); | |
| }, | |
| create: (data: Partial<Feedback>) => request('/feedback', { method: 'POST', body: JSON.stringify(data) }), | |
| update: (id: string, data: { status?: string, reply?: string }) => request(`/feedback/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| ignoreAll: (targetId: string) => request('/feedback/ignore-all', { method: 'POST', body: JSON.stringify({ targetId }) }), | |
| }, | |
| ai: { | |
| chat: (data: { text?: string, audio?: string, history?: { role: string, text?: string }[], enableThinking?: boolean, overrideSystemPrompt?: string, disableAudio?: boolean }) => request('/ai/chat', { method: 'POST', body: JSON.stringify(data) }), | |
| evaluate: (data: { question: string, audio?: string, image?: string }) => request('/ai/evaluate', { method: 'POST', body: JSON.stringify(data) }), | |
| resetPool: () => request('/ai/reset-pool', { method: 'POST' }), | |
| getStats: () => request('/ai/stats'), // NEW Detailed Stats | |
| }, | |
| todos: { // NEW | |
| getAll: () => request('/todos'), | |
| add: (content: string) => request('/todos', { method: 'POST', body: JSON.stringify({ content }) }), | |
| update: (id: string, data: Partial<Todo>) => request(`/todos/${id}`, { method: 'PUT', body: JSON.stringify(data) }), | |
| delete: (id: string) => request(`/todos/${id}`, { method: 'DELETE' }), | |
| } | |
| }; | |