Spaces:
Running
Running
Upload 35 files
Browse files- pages/Dashboard.tsx +9 -4
- 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
//
|
|
|
|
| 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 |
-
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
// 4. Weighted Random Logic
|
| 174 |
let selectedPrize = defaultPrize;
|
| 175 |
let rewardType = 'CONSOLATION'; // Default to consolation
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 };
|