dvc890 commited on
Commit
58175ca
·
verified ·
1 Parent(s): cb7f087

Upload 35 files

Browse files
Files changed (2) hide show
  1. pages/Dashboard.tsx +9 -4
  2. server.js +54 -33
pages/Dashboard.tsx CHANGED
@@ -75,6 +75,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
75
  if (isAdmin && classes.length > 0) {
76
  const grades = Array.from(new Set((classes as ClassInfo[]).map((c: ClassInfo) => c.grade))).sort(sortGrades);
77
  if (grades.length > 0) {
 
78
  setViewGrade(grades[0] as string);
79
  }
80
  }
@@ -108,7 +109,8 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
108
  }, []);
109
 
110
  useEffect(() => {
111
- if (showSchedule) fetchSchedules();
 
112
  }, [showSchedule, viewGrade]);
113
 
114
  const fetchSchedules = async () => {
@@ -116,6 +118,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
116
  const params: any = {};
117
  if (isAdmin) {
118
  if (!viewGrade) return;
 
119
  params.grade = viewGrade;
120
  } else {
121
  if (currentUser?.role === 'TEACHER') {
@@ -168,7 +171,9 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
168
  };
169
 
170
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
171
- const modalClassOptions = isAdmin
 
 
172
  ? classList.filter(c => c.grade === viewGrade).map(c => c.grade + c.className).sort(sortClasses)
173
  : classList.map(c => c.grade + c.className).sort(sortClasses);
174
 
@@ -272,11 +277,11 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
272
  {isAdmin && (
273
  <div className="flex bg-gray-100 rounded-lg p-1">
274
  <select
275
- className="bg-transparent border-none text-sm p-2 focus:ring-0 font-medium text-gray-700 cursor-pointer"
276
  value={viewGrade}
277
  onChange={e => setViewGrade(e.target.value)}
278
  >
279
- {uniqueGrades.length > 0 ? uniqueGrades.map(g => <option key={g} value={g}>{g}</option>) : <option>无年级数据</option>}
280
  </select>
281
  </div>
282
  )}
 
75
  if (isAdmin && classes.length > 0) {
76
  const grades = Array.from(new Set((classes as ClassInfo[]).map((c: ClassInfo) => c.grade))).sort(sortGrades);
77
  if (grades.length > 0) {
78
+ // IMPORTANT: Default viewGrade to the first available grade so schedule isn't empty
79
  setViewGrade(grades[0] as string);
80
  }
81
  }
 
109
  }, []);
110
 
111
  useEffect(() => {
112
+ // Fetch schedules whenever showSchedule opens OR viewGrade changes
113
+ if (showSchedule || viewGrade) fetchSchedules();
114
  }, [showSchedule, viewGrade]);
115
 
116
  const fetchSchedules = async () => {
 
118
  const params: any = {};
119
  if (isAdmin) {
120
  if (!viewGrade) return;
121
+ // Sending grade parameter which backend will now treat as a regex filter for className
122
  params.grade = viewGrade;
123
  } else {
124
  if (currentUser?.role === 'TEACHER') {
 
171
  };
172
 
173
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
174
+
175
+ // Filter class options in the modal based on currently selected viewGrade (if Admin)
176
+ const modalClassOptions = isAdmin && viewGrade
177
  ? classList.filter(c => c.grade === viewGrade).map(c => c.grade + c.className).sort(sortClasses)
178
  : classList.map(c => c.grade + c.className).sort(sortClasses);
179
 
 
277
  {isAdmin && (
278
  <div className="flex bg-gray-100 rounded-lg p-1">
279
  <select
280
+ className="bg-transparent border-none text-sm p-2 focus:ring-0 font-medium text-gray-700 cursor-pointer outline-none"
281
  value={viewGrade}
282
  onChange={e => setViewGrade(e.target.value)}
283
  >
284
+ {uniqueGrades.length > 0 ? uniqueGrades.map(g => <option key={g} value={g}>{g}</option>) : <option value="">无年级数据</option>}
285
  </select>
286
  </div>
287
  )}
server.js CHANGED
@@ -135,8 +135,7 @@ app.post('/api/games/lucky-draw', async (req, res) => {
135
  // 1. Get Student
136
  const student = await Student.findById(studentId);
137
  if (!student) return res.status(404).json({ error: 'Student not found' });
138
- if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '您的抽奖次数已用完' });
139
-
140
  // 2. Get Config
141
  const filter = schoolId ? { $or: [{ schoolId }, { schoolId: { $exists: false } }] } : {};
142
  const config = await LuckyDrawConfigModel.findOne(filter);
@@ -147,6 +146,8 @@ app.post('/api/games/lucky-draw', async (req, res) => {
147
  // 2.5 Daily Limit Check
148
  // Only limit if it's a STUDENT drawing for themselves. Teachers/Admins bypass limits.
149
  if (userRole === 'STUDENT') {
 
 
150
  const today = new Date().toISOString().split('T')[0];
151
  let dailyLog = student.dailyDrawLog || { date: today, count: 0 };
152
 
@@ -160,49 +161,57 @@ app.post('/api/games/lucky-draw', async (req, res) => {
160
 
161
  // Increment daily count
162
  dailyLog.count += 1;
163
- // Note: We'll save this along with drawAttempts deduction
 
164
  student.dailyDrawLog = dailyLog;
 
 
 
 
 
 
165
  }
166
 
167
  // 3. Global Inventory Check
168
  const availablePrizes = prizes.filter(p => (p.count === undefined || p.count > 0));
169
  if (availablePrizes.length === 0) {
170
- return res.status(409).json({ error: 'POOL_EMPTY', message: '奖品池已见底' });
 
171
  }
172
 
173
  // 4. Weighted Random Logic
174
  let selectedPrize = defaultPrize;
175
  let rewardType = 'CONSOLATION'; // Default to consolation
176
- const random = Math.random() * 100;
177
- let currentWeight = 0;
178
- let matchedPrize = null;
179
-
180
- for (const p of availablePrizes) {
181
- currentWeight += p.probability || 0;
182
- if (random <= currentWeight) {
183
- matchedPrize = p;
184
- break;
185
- }
186
- }
187
-
188
- if (matchedPrize) {
189
- selectedPrize = matchedPrize.name;
190
- rewardType = 'ITEM'; // It's a real prize
191
- // Only decrease count if it's not infinite (undefined)
192
- if (matchedPrize.count !== undefined && matchedPrize.count > 0) {
193
- if (config._id) {
194
- await LuckyDrawConfigModel.updateOne(
195
- { _id: config._id, "prizes.id": matchedPrize.id },
196
- { $inc: { "prizes.$.count": -1 } }
197
- );
198
- }
199
- }
 
 
 
 
200
  }
201
 
202
- // 5. Consume Attempt & Save Daily Log
203
- student.drawAttempts -= 1;
204
- await student.save();
205
-
206
  // 6. Record Reward
207
  // Note: Consolation prizes are recorded but handled differently in UI
208
  await StudentRewardModel.create({
@@ -399,7 +408,19 @@ app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelet
399
  app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
400
  app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
401
 
402
- app.get('/api/schedules', async (req, res) => { res.json(await ScheduleModel.find({...getQueryFilter(req), ...req.query})); });
 
 
 
 
 
 
 
 
 
 
 
 
403
  app.post('/api/schedules', async (req, res) => {
404
  // For upsert, we need to include schoolId in the query to avoid overwriting other schools' schedules
405
  const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period };
 
135
  // 1. Get Student
136
  const student = await Student.findById(studentId);
137
  if (!student) return res.status(404).json({ error: 'Student not found' });
138
+
 
139
  // 2. Get Config
140
  const filter = schoolId ? { $or: [{ schoolId }, { schoolId: { $exists: false } }] } : {};
141
  const config = await LuckyDrawConfigModel.findOne(filter);
 
146
  // 2.5 Daily Limit Check
147
  // Only limit if it's a STUDENT drawing for themselves. Teachers/Admins bypass limits.
148
  if (userRole === 'STUDENT') {
149
+ if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '您的抽奖次数已用完' });
150
+
151
  const today = new Date().toISOString().split('T')[0];
152
  let dailyLog = student.dailyDrawLog || { date: today, count: 0 };
153
 
 
161
 
162
  // Increment daily count
163
  dailyLog.count += 1;
164
+ // Consume Attempt
165
+ student.drawAttempts -= 1;
166
  student.dailyDrawLog = dailyLog;
167
+ await student.save();
168
+ } else {
169
+ // Teacher/Admin proxy draw - just consume attempts, no daily limit
170
+ if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '该学生抽奖次数已用完' });
171
+ student.drawAttempts -= 1;
172
+ await student.save();
173
  }
174
 
175
  // 3. Global Inventory Check
176
  const availablePrizes = prizes.filter(p => (p.count === undefined || p.count > 0));
177
  if (availablePrizes.length === 0) {
178
+ // Even if pool empty, we consumed attempt? Maybe refund?
179
+ // For now, let's allow "Consolation" if pool empty
180
  }
181
 
182
  // 4. Weighted Random Logic
183
  let selectedPrize = defaultPrize;
184
  let rewardType = 'CONSOLATION'; // Default to consolation
185
+
186
+ // If pool is empty, force consolation
187
+ if (availablePrizes.length > 0) {
188
+ const random = Math.random() * 100;
189
+ let currentWeight = 0;
190
+ let matchedPrize = null;
191
+
192
+ for (const p of availablePrizes) {
193
+ currentWeight += p.probability || 0;
194
+ if (random <= currentWeight) {
195
+ matchedPrize = p;
196
+ break;
197
+ }
198
+ }
199
+
200
+ if (matchedPrize) {
201
+ selectedPrize = matchedPrize.name;
202
+ rewardType = 'ITEM'; // It's a real prize
203
+ // Only decrease count if it's not infinite (undefined)
204
+ if (matchedPrize.count !== undefined && matchedPrize.count > 0) {
205
+ if (config._id) {
206
+ await LuckyDrawConfigModel.updateOne(
207
+ { _id: config._id, "prizes.id": matchedPrize.id },
208
+ { $inc: { "prizes.$.count": -1 } }
209
+ );
210
+ }
211
+ }
212
+ }
213
  }
214
 
 
 
 
 
215
  // 6. Record Reward
216
  // Note: Consolation prizes are recorded but handled differently in UI
217
  await StudentRewardModel.create({
 
408
  app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
409
  app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
410
 
411
+ // SCHEDULES API: MODIFIED TO SUPPORT GRADE REGEX QUERY
412
+ app.get('/api/schedules', async (req, res) => {
413
+ const query = { ...getQueryFilter(req), ...req.query };
414
+
415
+ // IMPORTANT: If 'grade' is passed (e.g. from Admin Dashboard), filter className by startsWith
416
+ if (query.grade) {
417
+ query.className = { $regex: '^' + query.grade };
418
+ delete query.grade; // Remove original grade param as it's not in schema
419
+ }
420
+
421
+ res.json(await ScheduleModel.find(query));
422
+ });
423
+
424
  app.post('/api/schedules', async (req, res) => {
425
  // For upsert, we need to include schoolId in the query to avoid overwriting other schools' schedules
426
  const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period };