dvc890 commited on
Commit
07d35d2
·
verified ·
1 Parent(s): 31237e3

Upload 51 files

Browse files
pages/Attendance.tsx CHANGED
@@ -99,7 +99,7 @@ export const AttendancePage: React.FC = () => {
99
  else if (current === 'Leave') nextStatus = 'Absent';
100
  else if (current === 'Absent') nextStatus = 'Present';
101
  if (!current) nextStatus = 'Present';
102
- await api.attendance.update({ studentId, date, status: nextStatus as 'Present' | 'Absent' | 'Leave' });
103
  loadData();
104
  };
105
 
@@ -272,4 +272,4 @@ export const AttendancePage: React.FC = () => {
272
  )}
273
  </div>
274
  );
275
- };
 
99
  else if (current === 'Leave') nextStatus = 'Absent';
100
  else if (current === 'Absent') nextStatus = 'Present';
101
  if (!current) nextStatus = 'Present';
102
+ await api.attendance.update({ studentId, date, status: nextStatus });
103
  loadData();
104
  };
105
 
 
272
  )}
273
  </div>
274
  );
275
+ };
pages/ScoreList.tsx CHANGED
@@ -126,7 +126,7 @@ export const ScoreList: React.FC = () => {
126
  semester: importSemester,
127
  type: formData.type,
128
  examName: formData.examName,
129
- status: formData.status as ExamStatus
130
  });
131
  setIsAddOpen(false);
132
  setFormData({ studentId: '', score: '', type: 'Final', examName: '', status: 'Normal' });
@@ -477,4 +477,4 @@ export const ScoreList: React.FC = () => {
477
  )}
478
  </div>
479
  );
480
- };
 
126
  semester: importSemester,
127
  type: formData.type,
128
  examName: formData.examName,
129
+ status: formData.status
130
  });
131
  setIsAddOpen(false);
132
  setFormData({ studentId: '', score: '', type: 'Final', examName: '', status: 'Normal' });
 
477
  )}
478
  </div>
479
  );
480
+ };
pages/StudentList.tsx CHANGED
@@ -163,7 +163,7 @@ export const StudentList: React.FC = () => {
163
  const payload = {
164
  ...formData,
165
  birthday: '2015-01-01',
166
- status: 'Enrolled' as const,
167
  gender: formData.gender as any
168
  };
169
 
@@ -507,4 +507,4 @@ export const StudentList: React.FC = () => {
507
  )}
508
  </div>
509
  );
510
- };
 
163
  const payload = {
164
  ...formData,
165
  birthday: '2015-01-01',
166
+ status: 'Enrolled',
167
  gender: formData.gender as any
168
  };
169
 
 
507
  )}
508
  </div>
509
  );
510
+ };
pages/UserList.tsx CHANGED
@@ -2,7 +2,7 @@
2
  import React, { useState, useEffect } from 'react';
3
  import { User, UserRole, UserStatus, School, ClassInfo } from '../types';
4
  import { api } from '../services/api';
5
- import { Loader2, Check, X, Trash2, Edit, Briefcase, GraduationCap, AlertCircle, Bot, KeyRound } from 'lucide-react';
6
 
7
  export const UserList: React.FC = () => {
8
  const [users, setUsers] = useState<User[]>([]);
@@ -51,16 +51,6 @@ export const UserList: React.FC = () => {
51
  }
52
  };
53
 
54
- const handleResetPassword = async (user: User) => {
55
- if (!confirm(`确定要重置 ${user.trueName || user.username} 的密码为 123456 吗?`)) return;
56
- try {
57
- await api.users.resetPassword(user._id || String(user.id));
58
- alert('密码已重置为 123456');
59
- } catch (e: any) {
60
- alert('重置失败: ' + e.message);
61
- }
62
- };
63
-
64
  const handleRoleChange = async (user: User, newRole: UserRole) => {
65
  if (user.username === 'admin') return alert('无法修改超级管理员');
66
  if (isPrincipal && newRole === UserRole.ADMIN) return alert('权限不足:校长无法将用户提升为超级管理员');
@@ -124,7 +114,6 @@ export const UserList: React.FC = () => {
124
 
125
  if (isTeacher) {
126
  if (user.role !== UserRole.STUDENT) return false;
127
- // Strict homeroom filter for teachers viewing the list
128
  const teacherClass = (currentUser.homeroomClass || '').trim();
129
  const studentClass = (user.homeroomClass || '').trim();
130
  if (teacherClass && studentClass === teacherClass) {
@@ -188,11 +177,6 @@ export const UserList: React.FC = () => {
188
  {filteredUsers.map(user => {
189
  const isSelf = !!(currentUser && (user._id === currentUser._id || user.username === currentUser.username));
190
  const hasApp = user.classApplication && user.classApplication.status === 'PENDING';
191
-
192
- // Permission Logic for Reset Password Button
193
- const canReset = isAdmin ||
194
- (isPrincipal && user.role !== UserRole.ADMIN && user.role !== UserRole.PRINCIPAL) ||
195
- (isTeacher && user.role === UserRole.STUDENT);
196
 
197
  return (
198
  <tr key={user._id || user.id} className="hover:bg-gray-50">
@@ -282,19 +266,12 @@ export const UserList: React.FC = () => {
282
  </td>
283
  )}
284
 
285
- <td className="px-4 py-3 text-right space-x-2 flex justify-end">
286
  {user.status === UserStatus.PENDING && (
287
  <button onClick={() => handleApprove(user)} className="text-green-600 hover:bg-green-50 p-1 rounded border border-green-200" title="通过注册审核">
288
  <span className="flex items-center text-xs font-bold"><Check size={14} className="mr-1"/> 激活</span>
289
  </button>
290
  )}
291
-
292
- {canReset && (
293
- <button onClick={() => handleResetPassword(user)} className="text-orange-500 hover:bg-orange-50 p-1.5 rounded" title="重置密码为 123456">
294
- <KeyRound size={18}/>
295
- </button>
296
- )}
297
-
298
  {(isAdmin || isPrincipal) && (
299
  <button
300
  onClick={() => handleDelete(user)}
 
2
  import React, { useState, useEffect } from 'react';
3
  import { User, UserRole, UserStatus, School, ClassInfo } from '../types';
4
  import { api } from '../services/api';
5
+ import { Loader2, Check, X, Trash2, Edit, Briefcase, GraduationCap, AlertCircle, Bot } from 'lucide-react';
6
 
7
  export const UserList: React.FC = () => {
8
  const [users, setUsers] = useState<User[]>([]);
 
51
  }
52
  };
53
 
 
 
 
 
 
 
 
 
 
 
54
  const handleRoleChange = async (user: User, newRole: UserRole) => {
55
  if (user.username === 'admin') return alert('无法修改超级管理员');
56
  if (isPrincipal && newRole === UserRole.ADMIN) return alert('权限不足:校长无法将用户提升为超级管理员');
 
114
 
115
  if (isTeacher) {
116
  if (user.role !== UserRole.STUDENT) return false;
 
117
  const teacherClass = (currentUser.homeroomClass || '').trim();
118
  const studentClass = (user.homeroomClass || '').trim();
119
  if (teacherClass && studentClass === teacherClass) {
 
177
  {filteredUsers.map(user => {
178
  const isSelf = !!(currentUser && (user._id === currentUser._id || user.username === currentUser.username));
179
  const hasApp = user.classApplication && user.classApplication.status === 'PENDING';
 
 
 
 
 
180
 
181
  return (
182
  <tr key={user._id || user.id} className="hover:bg-gray-50">
 
266
  </td>
267
  )}
268
 
269
+ <td className="px-4 py-3 text-right space-x-2">
270
  {user.status === UserStatus.PENDING && (
271
  <button onClick={() => handleApprove(user)} className="text-green-600 hover:bg-green-50 p-1 rounded border border-green-200" title="通过注册审核">
272
  <span className="flex items-center text-xs font-bold"><Check size={14} className="mr-1"/> 激活</span>
273
  </button>
274
  )}
 
 
 
 
 
 
 
275
  {(isAdmin || isPrincipal) && (
276
  <button
277
  onClick={() => handleDelete(user)}
pages/WishesAndFeedback.tsx CHANGED
@@ -122,7 +122,7 @@ export const WishesAndFeedback: React.FC = () => {
122
  const content = type === 'ACADEMIC' ? feedbackContent : wishContent;
123
  if (!content.trim()) return alert('请输入内容');
124
  try {
125
- const payload: Partial<Feedback> = { creatorId: type === 'ACADEMIC' ? (studentInfo?._id || String(studentInfo?.id)) : currentUser?._id, creatorName: type === 'ACADEMIC' ? studentInfo?.name : (currentUser?.trueName || currentUser?.username), creatorRole: currentUser?.role, content: content, type: type, status: 'PENDING' as const };
126
  if (type === 'ACADEMIC') { if (!selectedTeacherId) return alert('请选择反馈对象'); const targetTeacher = teachers.find(t => t._id === selectedTeacherId); payload.targetId = selectedTeacherId; payload.targetName = targetTeacher?.trueName || targetTeacher?.username; } else { payload.targetId = 'ADMIN'; payload.targetName = '系统管理员'; }
127
  await api.feedback.create(payload); alert('提交成功'); if (type === 'ACADEMIC') setFeedbackContent(''); else setWishContent(''); loadData();
128
  } catch(e) { alert('提交失败'); }
@@ -182,4 +182,4 @@ export const WishesAndFeedback: React.FC = () => {
182
  </div>
183
  </div>
184
  );
185
- };
 
122
  const content = type === 'ACADEMIC' ? feedbackContent : wishContent;
123
  if (!content.trim()) return alert('请输入内容');
124
  try {
125
+ const payload: Partial<Feedback> = { creatorId: type === 'ACADEMIC' ? (studentInfo?._id || String(studentInfo?.id)) : currentUser?._id, creatorName: type === 'ACADEMIC' ? studentInfo?.name : (currentUser?.trueName || currentUser?.username), creatorRole: currentUser?.role, content: content, type: type, status: 'PENDING' };
126
  if (type === 'ACADEMIC') { if (!selectedTeacherId) return alert('请选择反馈对象'); const targetTeacher = teachers.find(t => t._id === selectedTeacherId); payload.targetId = selectedTeacherId; payload.targetName = targetTeacher?.trueName || targetTeacher?.username; } else { payload.targetId = 'ADMIN'; payload.targetName = '系统管理员'; }
127
  await api.feedback.create(payload); alert('提交成功'); if (type === 'ACADEMIC') setFeedbackContent(''); else setWishContent(''); loadData();
128
  } catch(e) { alert('提交失败'); }
 
182
  </div>
183
  </div>
184
  );
185
+ };
server.js CHANGED
@@ -48,6 +48,7 @@ const connectDB = async () => {
48
  await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
49
  console.log('✅ MongoDB 连接成功 (Real Data)');
50
 
 
51
  try {
52
  await ScheduleModel.collection.dropIndex('schoolId_1_className_1_dayOfWeek_1_period_1');
53
  console.log('✅ Dropped restrictive schedule index');
@@ -112,61 +113,7 @@ const generateStudentNo = async () => {
112
  // MOUNT AI ROUTES
113
  app.use('/api/ai', aiRoutes);
114
 
115
- // --- USER MENU ORDER ---
116
- app.put('/api/users/:id/menu-order', async (req, res) => {
117
- const { menuOrder } = req.body;
118
- await User.findByIdAndUpdate(req.params.id, { menuOrder });
119
- res.json({ success: true });
120
- });
121
-
122
- // --- PASSWORD RESET ENDPOINT ---
123
- app.post('/api/users/:id/reset-password', async (req, res) => {
124
- const targetUserId = req.params.id;
125
- const requesterRole = req.headers['x-user-role'];
126
- const requesterUsername = req.headers['x-user-username'];
127
- const requesterSchoolId = req.headers['x-school-id'];
128
-
129
- try {
130
- const targetUser = await User.findById(targetUserId);
131
- if (!targetUser) return res.status(404).json({ error: 'Target user not found' });
132
-
133
- const requester = await User.findOne({ username: requesterUsername });
134
- if (!requester) return res.status(401).json({ error: 'Requester not found' });
135
-
136
- let hasPermission = false;
137
-
138
- if (requesterRole === 'ADMIN') {
139
- // Admins can reset anyone
140
- hasPermission = true;
141
- } else if (requesterRole === 'PRINCIPAL') {
142
- // Principals can reset anyone in their school except other Admins
143
- if (targetUser.schoolId === requesterSchoolId && targetUser.role !== 'ADMIN') {
144
- hasPermission = true;
145
- }
146
- } else if (requesterRole === 'TEACHER') {
147
- // Teachers can only reset Students in their Homeroom Class
148
- if (targetUser.role === 'STUDENT' && requester.homeroomClass) {
149
- // Check if student belongs to teacher's homeroom class
150
- // User model stores class name in 'homeroomClass' for students as well (from registration/update logic)
151
- if (targetUser.homeroomClass === requester.homeroomClass) {
152
- hasPermission = true;
153
- }
154
- }
155
- }
156
-
157
- if (!hasPermission) {
158
- return res.status(403).json({ error: 'PERMISSION_DENIED', message: '您没有权限重置该用户的密码' });
159
- }
160
-
161
- targetUser.password = '123456';
162
- await targetUser.save();
163
-
164
- res.json({ success: true });
165
- } catch (e) {
166
- console.error("Reset password error:", e);
167
- res.status(500).json({ error: e.message });
168
- }
169
- });
170
 
171
  // --- TODO LIST ENDPOINTS ---
172
  app.get('/api/todos', async (req, res) => {
@@ -195,13 +142,14 @@ app.delete('/api/todos/:id', async (req, res) => {
195
  res.json({ success: true });
196
  });
197
 
198
- // --- SCHEDULE ENDPOINTS ---
199
  app.get('/api/schedules', async (req, res) => {
200
  const query = { ...getQueryFilter(req), ...req.query };
201
  if (query.grade) { query.className = { $regex: '^' + query.grade }; delete query.grade; }
202
  res.json(await ScheduleModel.find(query));
203
  });
204
 
 
205
  app.put('/api/schedules/:id', async (req, res) => {
206
  try {
207
  await ScheduleModel.findByIdAndUpdate(req.params.id, req.body);
@@ -211,8 +159,10 @@ app.put('/api/schedules/:id', async (req, res) => {
211
  }
212
  });
213
 
 
214
  app.post('/api/schedules', async (req, res) => {
215
  try {
 
216
  const filter = {
217
  className: req.body.className,
218
  dayOfWeek: req.body.dayOfWeek,
@@ -225,12 +175,14 @@ app.post('/api/schedules', async (req, res) => {
225
  await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true});
226
  res.json({});
227
  } catch (e) {
 
228
  res.status(500).json({ error: e.message });
229
  }
230
  });
231
 
232
  app.delete('/api/schedules', async (req, res) => {
233
  try {
 
234
  if (req.query.id) {
235
  await ScheduleModel.findByIdAndDelete(req.query.id);
236
  } else {
@@ -242,7 +194,14 @@ app.delete('/api/schedules', async (req, res) => {
242
  }
243
  });
244
 
245
- // ... (Rest of existing routes)
 
 
 
 
 
 
 
246
  app.get('/api/classes/:className/teachers', async (req, res) => {
247
  const { className } = req.params;
248
  const schoolId = req.headers['x-school-id'];
@@ -526,9 +485,13 @@ app.put('/api/classes/:id', async (req, res) => {
526
  const teachers = await User.find({ _id: { $in: newTeacherIds } });
527
  displayTeacherName = teachers.map(t => t.trueName || t.username).join(', ');
528
  }
 
 
529
  const updatePayload = { grade, className, teacherName: displayTeacherName, homeroomTeacherIds: newTeacherIds };
530
  if (periodConfig) updatePayload.periodConfig = periodConfig;
 
531
  await ClassModel.findByIdAndUpdate(classId, updatePayload);
 
532
  if (oldFullClass !== newFullClass) {
533
  await Student.updateMany({ className: oldFullClass, schoolId: sId }, { className: newFullClass });
534
  await User.updateMany({ homeroomClass: oldFullClass, schoolId: sId }, { homeroomClass: newFullClass });
 
48
  await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
49
  console.log('✅ MongoDB 连接成功 (Real Data)');
50
 
51
+ // FIX: Drop the restrictive index that prevents multiple schedules per slot
52
  try {
53
  await ScheduleModel.collection.dropIndex('schoolId_1_className_1_dayOfWeek_1_period_1');
54
  console.log('✅ Dropped restrictive schedule index');
 
113
  // MOUNT AI ROUTES
114
  app.use('/api/ai', aiRoutes);
115
 
116
+ // ... (Rest of Existing Routes) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  // --- TODO LIST ENDPOINTS ---
119
  app.get('/api/todos', async (req, res) => {
 
142
  res.json({ success: true });
143
  });
144
 
145
+ // --- UPDATED SCHEDULE ENDPOINTS ---
146
  app.get('/api/schedules', async (req, res) => {
147
  const query = { ...getQueryFilter(req), ...req.query };
148
  if (query.grade) { query.className = { $regex: '^' + query.grade }; delete query.grade; }
149
  res.json(await ScheduleModel.find(query));
150
  });
151
 
152
+ // NEW: Update by ID (Exact Update)
153
  app.put('/api/schedules/:id', async (req, res) => {
154
  try {
155
  await ScheduleModel.findByIdAndUpdate(req.params.id, req.body);
 
159
  }
160
  });
161
 
162
+ // Create or Update by Logic (Upsert)
163
  app.post('/api/schedules', async (req, res) => {
164
  try {
165
+ // Updated Filter: Include weekType to allow separate ODD/EVEN records for same slot
166
  const filter = {
167
  className: req.body.className,
168
  dayOfWeek: req.body.dayOfWeek,
 
175
  await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true});
176
  res.json({});
177
  } catch (e) {
178
+ console.error("Save schedule error:", e);
179
  res.status(500).json({ error: e.message });
180
  }
181
  });
182
 
183
  app.delete('/api/schedules', async (req, res) => {
184
  try {
185
+ // Support deleting by ID if provided
186
  if (req.query.id) {
187
  await ScheduleModel.findByIdAndDelete(req.query.id);
188
  } else {
 
194
  }
195
  });
196
 
197
+ // --- USER MENU ORDER ---
198
+ app.put('/api/users/:id/menu-order', async (req, res) => {
199
+ const { menuOrder } = req.body;
200
+ await User.findByIdAndUpdate(req.params.id, { menuOrder });
201
+ res.json({ success: true });
202
+ });
203
+
204
+ // ... (Rest of existing routes unchanged) ...
205
  app.get('/api/classes/:className/teachers', async (req, res) => {
206
  const { className } = req.params;
207
  const schoolId = req.headers['x-school-id'];
 
485
  const teachers = await User.find({ _id: { $in: newTeacherIds } });
486
  displayTeacherName = teachers.map(t => t.trueName || t.username).join(', ');
487
  }
488
+
489
+ // FIX: Explicitly update periodConfig if present in the body
490
  const updatePayload = { grade, className, teacherName: displayTeacherName, homeroomTeacherIds: newTeacherIds };
491
  if (periodConfig) updatePayload.periodConfig = periodConfig;
492
+
493
  await ClassModel.findByIdAndUpdate(classId, updatePayload);
494
+
495
  if (oldFullClass !== newFullClass) {
496
  await Student.updateMany({ className: oldFullClass, schoolId: sId }, { className: newFullClass });
497
  await User.updateMany({ homeroomClass: oldFullClass, schoolId: sId }, { homeroomClass: newFullClass });
services/api.ts CHANGED
@@ -1,74 +1,113 @@
1
- import { User, Student, ClassInfo, Course, Score, Subject, Exam, Schedule, School, Notification, SystemConfig, GameSession, GameTeam, StudentReward, LuckyDrawConfig, AchievementConfig, AchievementItem, ExchangeRule, TeacherExchangeConfig, StudentAchievement, Attendance, SchoolCalendarEntry, Wish, Feedback, Todo, GameRewardConfig } from '../types';
2
 
3
- const API_BASE = '/api';
 
4
 
5
- const request = async (endpoint: string, options?: RequestInit) => {
6
- const token = localStorage.getItem('token');
7
- const headers = {
8
- 'Content-Type': 'application/json',
9
- ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
10
  // @ts-ignore
11
- ...(options?.headers || {})
12
- };
13
-
14
- const currentSchoolId = localStorage.getItem('admin_view_school_id');
15
- if (currentSchoolId) {
16
- // @ts-ignore
17
- headers['x-school-id'] = currentSchoolId;
18
  }
 
 
19
 
20
- const response = await fetch(`${API_BASE}${endpoint}`, {
21
- ...options,
22
- headers
23
- });
24
 
25
- if (response.status === 401) {
26
- localStorage.removeItem('user');
27
- localStorage.removeItem('token');
28
- // window.location.href = '/login';
29
- throw new Error('Unauthorized');
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
- const data = await response.json();
33
- if (!response.ok) {
34
- throw new Error(data.message || data.error || 'API Request Failed');
 
 
 
 
 
 
 
 
35
  }
36
- return data;
37
- };
38
 
39
  export const api = {
40
- init: () => {
41
- // Any initialization logic if needed
42
- },
43
  auth: {
44
- // @ts-ignore
45
- login: async (username, password) => {
46
- const res = await request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
47
- localStorage.setItem('user', JSON.stringify(res.user));
48
- localStorage.setItem('token', res.token);
49
- return res.user;
 
50
  },
51
- // @ts-ignore
52
- register: (data) => request('/auth/register', { method: 'POST', body: JSON.stringify(data) }),
53
- getCurrentUser: () => {
54
- const u = localStorage.getItem('user');
55
- return u ? JSON.parse(u) : null;
 
 
 
 
 
 
 
 
 
56
  },
57
  logout: () => {
 
58
  localStorage.removeItem('user');
59
- localStorage.removeItem('token');
60
  localStorage.removeItem('admin_view_school_id');
 
61
  },
62
- refreshSession: async () => {
 
63
  try {
64
- const res = await request('/auth/me');
65
- localStorage.setItem('user', JSON.stringify(res));
66
- return res;
67
- } catch (e) { return null; }
68
- },
69
- // @ts-ignore
70
- updateProfile: (data) => request('/auth/profile', { method: 'PUT', body: JSON.stringify(data) }),
 
 
 
 
 
 
 
 
 
71
  },
 
72
  users: {
73
  getAll: (options?: { global?: boolean; role?: string }) => {
74
  const params = new URLSearchParams();
@@ -81,139 +120,163 @@ export const api = {
81
  applyClass: (data: { userId: string, type: 'CLAIM'|'RESIGN', targetClass?: string, action: 'APPLY'|'APPROVE'|'REJECT' }) =>
82
  request('/users/class-application', { method: 'POST', body: JSON.stringify(data) }),
83
  getTeachersForClass: (className: string) => request(`/classes/${encodeURIComponent(className)}/teachers`),
84
- saveMenuOrder: (userId: string, order: string[]) => request(`/users/${userId}/menu-order`, { method: 'PUT', body: JSON.stringify({ menuOrder: order }) }),
85
- resetPassword: (id: string) => request(`/users/${id}/reset-password`, { method: 'POST' }),
86
  },
 
87
  students: {
88
  getAll: () => request('/students'),
89
- add: (data: Partial<Student>) => request('/students', { method: 'POST', body: JSON.stringify(data) }),
90
- update: (id: string, data: Partial<Student>) => request(`/students/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
91
- delete: (id: string) => request(`/students/${id}`, { method: 'DELETE' }),
92
- transfer: (data: { studentId: string, targetClass: string }) => request('/students/transfer', { method: 'POST', body: JSON.stringify(data) }),
93
- promote: (options: { teacherFollows: boolean }) => request('/students/promote', { method: 'POST', body: JSON.stringify(options) }),
94
  },
 
95
  classes: {
96
  getAll: () => request('/classes'),
97
- add: (data: Partial<ClassInfo>) => request('/classes', { method: 'POST', body: JSON.stringify(data) }),
98
- delete: (id: string | number) => request(`/classes/${id}`, { method: 'DELETE' }),
 
 
 
 
 
 
 
 
 
 
 
 
99
  },
 
100
  courses: {
101
  getAll: () => request('/courses'),
102
- add: (data: Partial<Course>) => request('/courses', { method: 'POST', body: JSON.stringify(data) }),
103
- update: (id: string, data: Partial<Course>) => request(`/courses/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
104
- delete: (id: string) => request(`/courses/${id}`, { method: 'DELETE' }),
105
  },
 
106
  scores: {
107
  getAll: () => request('/scores'),
108
- add: (data: Partial<Score>) => request('/scores', { method: 'POST', body: JSON.stringify(data) }),
109
- update: (id: string, data: Partial<Score>) => request(`/scores/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
110
- delete: (id: string) => request(`/scores/${id}`, { method: 'DELETE' }),
 
 
 
 
 
 
 
 
 
 
 
 
111
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  stats: {
113
- getSummary: () => request('/stats/summary'),
114
  },
 
115
  config: {
116
- get: () => request('/config'),
117
- save: (data: Partial<SystemConfig>) => request('/config', { method: 'PUT', body: JSON.stringify(data) }),
118
  getPublic: () => request('/public/config'),
 
119
  },
120
- schools: {
121
- getAll: () => request('/schools'),
122
- getPublic: () => request('/public/schools'),
123
- add: (data: Partial<School>) => request('/schools', { method: 'POST', body: JSON.stringify(data) }),
124
- update: (id: string, data: Partial<School>) => request(`/schools/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
125
- delete: (id: string) => request(`/schools/${id}`, { method: 'DELETE' }),
126
- },
127
  notifications: {
128
  getAll: (userId: string, role: string) => request(`/notifications?userId=${userId}&role=${role}`),
129
  },
 
130
  games: {
131
- getMountainSession: (className: string) => request(`/games/mountain?className=${encodeURIComponent(className)}`),
132
  saveMountainSession: (data: GameSession) => request('/games/mountain', { method: 'POST', body: JSON.stringify(data) }),
133
- getLuckyConfig: (className: string, ownerId: string) => request(`/games/lucky/config?className=${encodeURIComponent(className)}&ownerId=${ownerId}`),
134
- saveLuckyConfig: (data: LuckyDrawConfig) => request('/games/lucky/config', { method: 'POST', body: JSON.stringify(data) }),
135
- drawLucky: (studentId: string) => request('/games/lucky/draw', { method: 'POST', body: JSON.stringify({ studentId }) }),
136
- grantReward: (data: { studentId: string, count: number, rewardType: string, name: string }) => request('/games/rewards/grant', { method: 'POST', body: JSON.stringify(data) }),
137
- getMonsterConfig: (className: string) => request(`/games/monster/config?className=${encodeURIComponent(className)}`),
138
- saveMonsterConfig: (data: any) => request('/games/monster/config', { method: 'POST', body: JSON.stringify(data) }),
139
- getZenConfig: (className: string) => request(`/games/zen/config?className=${encodeURIComponent(className)}`),
140
- saveZenConfig: (data: any) => request('/games/zen/config', { method: 'POST', body: JSON.stringify(data) }),
141
  },
 
142
  achievements: {
143
- getConfig: (className: string) => request(`/achievements/config?className=${encodeURIComponent(className)}`),
144
- saveConfig: (data: AchievementConfig) => request('/achievements/config', { method: 'POST', body: JSON.stringify(data) }),
145
- getMyRules: () => request('/achievements/my-rules'),
146
- saveMyRules: (data: TeacherExchangeConfig) => request('/achievements/my-rules', { method: 'POST', body: JSON.stringify(data) }),
147
- grant: (data: { studentId: string, achievementId: string, semester: string }) => request('/achievements/grant', { method: 'POST', body: JSON.stringify(data) }),
148
- exchange: (data: { studentId: string, ruleId: string, teacherId?: string }) => request('/achievements/exchange', { method: 'POST', body: JSON.stringify(data) }),
149
- getStudentAchievements: (studentId: string, semester: string) => request(`/achievements/student/${studentId}?semester=${encodeURIComponent(semester)}`),
150
- getRulesByTeachers: (teacherIds: string[]) => request('/achievements/rules-by-teachers', { method: 'POST', body: JSON.stringify({ teacherIds }) }),
151
  },
 
152
  rewards: {
153
- getClassRewards: (page: number, limit: number, className: string) => request(`/rewards?page=${page}&limit=${limit}&className=${encodeURIComponent(className)}`),
154
- getMyRewards: (studentId: string, page: number, limit: number) => request(`/rewards/my/${studentId}?page=${page}&limit=${limit}`),
 
 
 
 
155
  addReward: (data: Partial<StudentReward>) => request('/rewards', { method: 'POST', body: JSON.stringify(data) }),
156
- redeem: (id: string) => request(`/rewards/${id}/redeem`, { method: 'PUT' }),
157
- delete: (id: string) => request(`/rewards/${id}`, { method: 'DELETE' }),
158
  update: (id: string, data: Partial<StudentReward>) => request(`/rewards/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
 
 
159
  },
160
- calendar: {
161
- get: (className?: string) => request(`/calendar?className=${encodeURIComponent(className || '')}`),
162
- add: (data: Partial<SchoolCalendarEntry>) => request('/calendar', { method: 'POST', body: JSON.stringify(data) }),
163
- delete: (id: string) => request(`/calendar/${id}`, { method: 'DELETE' }),
164
- },
165
- attendance: {
166
- get: (params: { className?: string, date?: string, studentId?: string }) => {
167
- const qs = new URLSearchParams(params as any).toString();
168
- return request(`/attendance?${qs}`);
169
- },
170
- checkIn: (data: { studentId: string, date: string }) => request('/attendance/checkin', { method: 'POST', body: JSON.stringify(data) }),
171
- applyLeave: (data: any) => request('/attendance/leave', { method: 'POST', body: JSON.stringify(data) }),
172
- batch: (data: { className: string, date: string }) => request('/attendance/batch', { method: 'POST', body: JSON.stringify(data) }),
173
- update: (data: Partial<Attendance>) => request('/attendance/update', { method: 'PUT', body: JSON.stringify(data) }),
174
- },
175
- schedules: {
176
- get: (params: { className: string }) => request(`/schedules?className=${encodeURIComponent(params.className)}`),
177
- save: (data: Partial<Schedule>) => request('/schedules', { method: 'POST', body: JSON.stringify(data) }),
178
- delete: (params: { className: string, dayOfWeek: number, period: number }) => request(`/schedules/delete`, { method: 'POST', body: JSON.stringify(params) }),
179
  },
 
180
  wishes: {
181
- getAll: (params: { studentId?: string, teacherId?: string }) => {
182
- const qs = new URLSearchParams(params as any).toString();
183
- return request(`/wishes?${qs}`);
184
- },
185
- create: (data: Partial<Wish>) => request('/wishes', { method: 'POST', body: JSON.stringify(data) }),
186
- fulfill: (id: string) => request(`/wishes/${id}/fulfill`, { method: 'PUT' }),
187
- randomFulfill: (teacherId: string) => request(`/wishes/random-fulfill`, { method: 'POST', body: JSON.stringify({ teacherId }) }),
188
  },
 
189
  feedback: {
190
- getAll: (params: any) => {
191
- const qs = new URLSearchParams(params).toString();
192
- return request(`/feedback?${qs}`);
193
- },
194
- create: (data: Partial<Feedback>) => request('/feedback', { method: 'POST', body: JSON.stringify(data) }),
195
- update: (id: string, data: Partial<Feedback>) => request(`/feedback/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
196
- ignoreAll: (targetId: string) => request(`/feedback/ignore-all`, { method: 'POST', body: JSON.stringify({ targetId }) }),
197
- },
198
- todos: {
199
- getAll: () => request('/todos'),
200
- // @ts-ignore
201
- add: (content: string) => request('/todos', { method: 'POST', body: JSON.stringify({ content }) }),
202
- update: (id: string, data: Partial<Todo>) => request(`/todos/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
203
- delete: (id: string) => request(`/todos/${id}`, { method: 'DELETE' }),
204
- },
205
- subjects: {
206
- getAll: () => request('/subjects'),
207
- add: (data: Partial<Subject>) => request('/subjects', { method: 'POST', body: JSON.stringify(data) }),
208
- update: (id: string, data: Partial<Subject>) => request(`/subjects/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
209
- delete: (id: string) => request(`/subjects/${id}`, { method: 'DELETE' }),
210
- },
211
- exams: {
212
- getAll: () => request('/exams'),
213
- save: (data: Partial<Exam>) => request('/exams', { method: 'POST', body: JSON.stringify(data) }),
214
  },
 
215
  ai: {
216
- evaluate: (data: { question: string, audio?: string, image?: string }) => request('/ai/evaluate', { method: 'POST', body: JSON.stringify(data) }),
 
217
  },
218
- batchDelete: (type: string, ids: string[]) => request(`/batch-delete`, { method: 'POST', body: JSON.stringify({ type, ids }) }),
219
- };
 
 
 
 
 
 
 
 
1
 
2
+ // ... existing imports
3
+ import { User, ClassInfo, SystemConfig, Subject, School, Schedule, GameSession, StudentReward, LuckyDrawConfig, Attendance, LeaveRequest, AchievementConfig, SchoolCalendarEntry, TeacherExchangeConfig, Wish, Feedback, AIChatMessage, Todo } from '../types';
4
 
5
+ // ... existing getBaseUrl ...
6
+ const getBaseUrl = () => {
7
+ let isProd = false;
8
+ try {
 
9
  // @ts-ignore
10
+ if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.PROD) {
11
+ isProd = true;
12
+ }
13
+ } catch (e) {}
14
+
15
+ if (isProd || (typeof window !== 'undefined' && window.location.port === '7860')) {
16
+ return '/api';
17
  }
18
+ return 'http://localhost:7860/api';
19
+ };
20
 
21
+ const API_BASE_URL = getBaseUrl();
 
 
 
22
 
23
+ async function request(endpoint: string, options: RequestInit = {}) {
24
+ const headers: any = { 'Content-Type': 'application/json', ...options.headers };
25
+
26
+ if (typeof window !== 'undefined') {
27
+ const currentUser = JSON.parse(localStorage.getItem('user') || 'null');
28
+ const selectedSchoolId = localStorage.getItem('admin_view_school_id');
29
+
30
+ if (currentUser?.role === 'ADMIN' && selectedSchoolId) {
31
+ headers['x-school-id'] = selectedSchoolId;
32
+ } else if (currentUser?.schoolId) {
33
+ headers['x-school-id'] = currentUser.schoolId;
34
+ }
35
+
36
+ if (currentUser?.role) {
37
+ headers['x-user-role'] = currentUser.role;
38
+ headers['x-user-username'] = currentUser.username;
39
+ }
40
  }
41
 
42
+ const res = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers });
43
+
44
+ if (!res.ok) {
45
+ if (res.status === 401) throw new Error('AUTH_FAILED');
46
+ const errorData = await res.json().catch(() => ({}));
47
+ const errorMessage = errorData.error || errorData.message || `Server Error: ${res.status}`;
48
+ if (errorData.error === 'PENDING_APPROVAL') throw new Error('PENDING_APPROVAL');
49
+ if (errorData.error === 'BANNED') throw new Error('BANNED');
50
+ if (errorData.error === 'CONFLICT') throw new Error(errorData.message);
51
+ if (errorData.error === 'INVALID_PASSWORD') throw new Error('INVALID_PASSWORD');
52
+ throw new Error(errorMessage);
53
  }
54
+ return res.json();
55
+ }
56
 
57
  export const api = {
58
+ init: () => console.log('🔗 API:', API_BASE_URL),
59
+
 
60
  auth: {
61
+ login: async (username: string, password: string): Promise<User> => {
62
+ const user = await request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
63
+ if (typeof window !== 'undefined') {
64
+ localStorage.setItem('user', JSON.stringify(user));
65
+ localStorage.removeItem('admin_view_school_id');
66
+ }
67
+ return user;
68
  },
69
+ refreshSession: async (): Promise<User | null> => {
70
+ try {
71
+ const user = await request('/auth/me');
72
+ if (typeof window !== 'undefined' && user) {
73
+ localStorage.setItem('user', JSON.stringify(user));
74
+ }
75
+ return user;
76
+ } catch (e) { return null; }
77
+ },
78
+ register: async (data: any): Promise<User> => {
79
+ return await request('/auth/register', { method: 'POST', body: JSON.stringify(data) });
80
+ },
81
+ updateProfile: async (data: any): Promise<any> => {
82
+ return await request('/auth/update-profile', { method: 'POST', body: JSON.stringify(data) });
83
  },
84
  logout: () => {
85
+ if (typeof window !== 'undefined') {
86
  localStorage.removeItem('user');
 
87
  localStorage.removeItem('admin_view_school_id');
88
+ }
89
  },
90
+ getCurrentUser: (): User | null => {
91
+ if (typeof window !== 'undefined') {
92
  try {
93
+ const stored = localStorage.getItem('user');
94
+ if (stored) return JSON.parse(stored);
95
+ } catch (e) {
96
+ localStorage.removeItem('user');
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ },
102
+
103
+ schools: {
104
+ getPublic: () => request('/public/schools'),
105
+ getAll: () => request('/schools'),
106
+ add: (data: School) => request('/schools', { method: 'POST', body: JSON.stringify(data) }),
107
+ update: (id: string, data: Partial<School>) => request(`/schools/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
108
+ delete: (id: string) => request(`/schools/${id}`, { method: 'DELETE' })
109
  },
110
+
111
  users: {
112
  getAll: (options?: { global?: boolean; role?: string }) => {
113
  const params = new URLSearchParams();
 
120
  applyClass: (data: { userId: string, type: 'CLAIM'|'RESIGN', targetClass?: string, action: 'APPLY'|'APPROVE'|'REJECT' }) =>
121
  request('/users/class-application', { method: 'POST', body: JSON.stringify(data) }),
122
  getTeachersForClass: (className: string) => request(`/classes/${encodeURIComponent(className)}/teachers`),
123
+ saveMenuOrder: (userId: string, order: string[]) => request(`/users/${userId}/menu-order`, { method: 'PUT', body: JSON.stringify({ menuOrder: order }) }), // NEW
 
124
  },
125
+
126
  students: {
127
  getAll: () => request('/students'),
128
+ add: (data: any) => request('/students', { method: 'POST', body: JSON.stringify(data) }),
129
+ update: (id: string, data: any) => request(`/students/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
130
+ delete: (id: string | number) => request(`/students/${id}`, { method: 'DELETE' }),
131
+ promote: (data: { teacherFollows: boolean }) => request('/students/promote', { method: 'POST', body: JSON.stringify(data) }),
132
+ transfer: (data: { studentId: string, targetClass: string }) => request('/students/transfer', { method: 'POST', body: JSON.stringify(data) })
133
  },
134
+
135
  classes: {
136
  getAll: () => request('/classes'),
137
+ add: (data: ClassInfo) => request('/classes', { method: 'POST', body: JSON.stringify(data) }),
138
+ delete: (id: string | number) => request(`/classes/${id}`, { method: 'DELETE' })
139
+ },
140
+
141
+ subjects: {
142
+ getAll: () => request('/subjects'),
143
+ add: (data: Subject) => request('/subjects', { method: 'POST', body: JSON.stringify(data) }),
144
+ update: (id: string | number, data: Partial<Subject>) => request(`/subjects/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
145
+ delete: (id: string | number) => request(`/subjects/${id}`, { method: 'DELETE' })
146
+ },
147
+
148
+ exams: {
149
+ getAll: () => request('/exams'),
150
+ save: (data: any) => request('/exams', { method: 'POST', body: JSON.stringify(data) })
151
  },
152
+
153
  courses: {
154
  getAll: () => request('/courses'),
155
+ add: (data: any) => request('/courses', { method: 'POST', body: JSON.stringify(data) }),
156
+ update: (id: string | number, data: any) => request(`/courses/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
157
+ delete: (id: string | number) => request(`/courses/${id}`, { method: 'DELETE' })
158
  },
159
+
160
  scores: {
161
  getAll: () => request('/scores'),
162
+ add: (data: any) => request('/scores', { method: 'POST', body: JSON.stringify(data) }),
163
+ update: (id: string | number, data: any) => request(`/scores/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
164
+ delete: (id: string | number) => request(`/scores/${id}`, { method: 'DELETE' })
165
+ },
166
+
167
+ schedules: {
168
+ get: (params: { className?: string; teacherName?: string; grade?: string }) => {
169
+ const qs = new URLSearchParams(params as any).toString();
170
+ return request(`/schedules?${qs}`);
171
+ },
172
+ save: (data: Schedule) => request('/schedules', { method: 'POST', body: JSON.stringify(data) }),
173
+ delete: (params: { className: string; dayOfWeek: number; period: number }) => {
174
+ const qs = new URLSearchParams(params as any).toString();
175
+ return request(`/schedules?${qs}`, { method: 'DELETE' });
176
+ }
177
  },
178
+
179
+ attendance: {
180
+ checkIn: (data: { studentId: string, date: string, status?: string }) => request('/attendance/check-in', { method: 'POST', body: JSON.stringify(data) }),
181
+ get: (params: { className?: string, date?: string, studentId?: string }) => {
182
+ const qs = new URLSearchParams(params as any).toString();
183
+ return request(`/attendance?${qs}`);
184
+ },
185
+ batch: (data: { className: string, date: string, status?: string }) => request('/attendance/batch', { method: 'POST', body: JSON.stringify(data) }),
186
+ update: (data: { studentId: string, date: string, status: string }) => request('/attendance/update', { method: 'PUT', body: JSON.stringify(data) }),
187
+ applyLeave: (data: { studentId: string, studentName: string, className: string, reason: string, startDate: string, endDate: string }) => request('/leave', { method: 'POST', body: JSON.stringify(data) }),
188
+ },
189
+
190
+ calendar: {
191
+ get: (className: string) => request(`/attendance/calendar?className=${className}`),
192
+ add: (data: SchoolCalendarEntry) => request('/attendance/calendar', { method: 'POST', body: JSON.stringify(data) }),
193
+ delete: (id: string) => request(`/attendance/calendar/${id}`, { method: 'DELETE' })
194
+ },
195
+
196
  stats: {
197
+ getSummary: () => request('/stats')
198
  },
199
+
200
  config: {
201
+ get: () => request('/config'),
 
202
  getPublic: () => request('/public/config'),
203
+ save: (data: SystemConfig) => request('/config', { method: 'POST', body: JSON.stringify(data) })
204
  },
205
+
 
 
 
 
 
 
206
  notifications: {
207
  getAll: (userId: string, role: string) => request(`/notifications?userId=${userId}&role=${role}`),
208
  },
209
+
210
  games: {
211
+ getMountainSession: (className: string) => request(`/games/mountain?className=${className}`),
212
  saveMountainSession: (data: GameSession) => request('/games/mountain', { method: 'POST', body: JSON.stringify(data) }),
213
+ getLuckyConfig: (className?: string, ownerId?: string) => request(`/games/lucky-config?className=${className || ''}${ownerId ? `&ownerId=${ownerId}` : ''}`),
214
+ saveLuckyConfig: (data: LuckyDrawConfig) => request('/games/lucky-config', { method: 'POST', body: JSON.stringify(data) }),
215
+ drawLucky: (studentId: string) => request('/games/lucky-draw', { method: 'POST', body: JSON.stringify({ studentId }) }),
216
+ grantReward: (data: { studentId: string, count: number, rewardType: string, name?: string }) => request('/games/grant-reward', { method: 'POST', body: JSON.stringify(data) }),
217
+ getMonsterConfig: (className: string) => request(`/games/monster-config?className=${className}`),
218
+ saveMonsterConfig: (data: any) => request('/games/monster-config', { method: 'POST', body: JSON.stringify(data) }),
219
+ getZenConfig: (className: string) => request(`/games/zen-config?className=${className}`),
220
+ saveZenConfig: (data: any) => request('/games/zen-config', { method: 'POST', body: JSON.stringify(data) }),
221
  },
222
+
223
  achievements: {
224
+ getConfig: (className: string) => request(`/achievements/config?className=${className}`),
225
+ saveConfig: (data: AchievementConfig) => request('/achievements/config', { method: 'POST', body: JSON.stringify(data) }),
226
+ getStudentAchievements: (studentId: string, semester?: string) => request(`/achievements/student?studentId=${studentId}${semester ? `&semester=${semester}` : ''}`),
227
+ grant: (data: { studentId: string, achievementId: string, semester: string }) => request('/achievements/grant', { method: 'POST', body: JSON.stringify(data) }),
228
+ exchange: (data: { studentId: string, ruleId: string, teacherId?: string }) => request('/achievements/exchange', { method: 'POST', body: JSON.stringify(data) }),
229
+ getMyRules: () => request('/achievements/teacher-rules'),
230
+ saveMyRules: (data: TeacherExchangeConfig) => request('/achievements/teacher-rules', { method: 'POST', body: JSON.stringify(data) }),
231
+ getRulesByTeachers: (teacherIds: string[]) => request(`/achievements/teacher-rules?teacherIds=${teacherIds.join(',')}`),
232
  },
233
+
234
  rewards: {
235
+ getMyRewards: (studentId: string, page = 1, limit = 20) => request(`/rewards?studentId=${studentId}&page=${page}&limit=${limit}&excludeType=CONSOLATION`),
236
+ getClassRewards: (page = 1, limit = 20, className?: string) => {
237
+ let qs = `scope=class&page=${page}&limit=${limit}&excludeType=CONSOLATION`;
238
+ if (className) qs += `&className=${className}`;
239
+ return request(`/rewards?${qs}`);
240
+ },
241
  addReward: (data: Partial<StudentReward>) => request('/rewards', { method: 'POST', body: JSON.stringify(data) }),
 
 
242
  update: (id: string, data: Partial<StudentReward>) => request(`/rewards/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
243
+ delete: (id: string) => request(`/rewards/${id}`, { method: 'DELETE' }),
244
+ redeem: (id: string) => request(`/rewards/${id}/redeem`, { method: 'POST' }),
245
  },
246
+
247
+ batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
248
+ return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  },
250
+
251
  wishes: {
252
+ getAll: (params: { teacherId?: string, studentId?: string, status?: string }) => {
253
+ const qs = new URLSearchParams(params as any).toString();
254
+ return request(`/wishes?${qs}`);
255
+ },
256
+ create: (data: Partial<Wish>) => request('/wishes', { method: 'POST', body: JSON.stringify(data) }),
257
+ fulfill: (id: string) => request(`/wishes/${id}/fulfill`, { method: 'POST' }),
258
+ randomFulfill: (teacherId: string) => request('/wishes/random-fulfill', { method: 'POST', body: JSON.stringify({ teacherId }) }),
259
  },
260
+
261
  feedback: {
262
+ getAll: (params: { targetId?: string, creatorId?: string, type?: string, status?: string }) => {
263
+ const qs = new URLSearchParams(params as any).toString();
264
+ return request(`/feedback?${qs}`);
265
+ },
266
+ create: (data: Partial<Feedback>) => request('/feedback', { method: 'POST', body: JSON.stringify(data) }),
267
+ update: (id: string, data: { status?: string, reply?: string }) => request(`/feedback/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
268
+ ignoreAll: (targetId: string) => request('/feedback/ignore-all', { method: 'POST', body: JSON.stringify({ targetId }) }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  },
270
+
271
  ai: {
272
+ chat: (data: { text?: string, audio?: string, history?: { role: string, text?: string }[] }) => request('/ai/chat', { method: 'POST', body: JSON.stringify(data) }),
273
+ evaluate: (data: { question: string, audio?: string, image?: string }) => request('/ai/evaluate', { method: 'POST', body: JSON.stringify(data) }),
274
  },
275
+
276
+ todos: { // NEW
277
+ getAll: () => request('/todos'),
278
+ add: (content: string) => request('/todos', { method: 'POST', body: JSON.stringify({ content }) }),
279
+ update: (id: string, data: Partial<Todo>) => request(`/todos/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
280
+ delete: (id: string) => request(`/todos/${id}`, { method: 'DELETE' }),
281
+ }
282
+ };