Spaces:
Sleeping
Sleeping
Upload 51 files
Browse files- pages/TeacherDashboard.tsx +51 -25
- server.js +34 -6
pages/TeacherDashboard.tsx
CHANGED
|
@@ -44,7 +44,7 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 44 |
|
| 45 |
// Modals & Editing
|
| 46 |
const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
|
| 47 |
-
const [editForm, setEditForm] = useState({ subject: '', teacherName: '', weekType: 'ALL' });
|
| 48 |
const [isPeriodSettingsOpen, setIsPeriodSettingsOpen] = useState(false);
|
| 49 |
const [tempPeriodConfig, setTempPeriodConfig] = useState<PeriodConfig[]>([]);
|
| 50 |
|
|
@@ -103,38 +103,56 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 103 |
finally { setLoading(false); }
|
| 104 |
};
|
| 105 |
|
| 106 |
-
const handleCellClick = (day: number, period: number) => {
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
(s.weekType === 'ALL' || s.weekType === weekType || weekType === 'ALL')
|
| 112 |
-
);
|
| 113 |
-
|
| 114 |
-
if (existingSlot) {
|
| 115 |
setEditForm({
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
| 119 |
});
|
| 120 |
} else {
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
-
setEditingCell({ day, period });
|
| 124 |
};
|
| 125 |
|
| 126 |
const handleSaveSchedule = async () => {
|
| 127 |
if (!homeroomClass || !editingCell) return;
|
| 128 |
if (!editForm.subject) return alert('请选择科目');
|
|
|
|
| 129 |
try {
|
| 130 |
-
|
| 131 |
className: homeroomClass,
|
| 132 |
dayOfWeek: editingCell.day,
|
| 133 |
period: editingCell.period,
|
| 134 |
subject: editForm.subject,
|
| 135 |
teacherName: editForm.teacherName,
|
| 136 |
weekType: editForm.weekType as any
|
| 137 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
setEditingCell(null);
|
| 139 |
const updated = await api.schedules.get({ className: homeroomClass });
|
| 140 |
setSchedules(updated);
|
|
@@ -145,10 +163,15 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 145 |
setConfirmModal({
|
| 146 |
isOpen: true,
|
| 147 |
title: '删除课程',
|
| 148 |
-
message: `确定要删除 ${s.subject} 吗?`,
|
| 149 |
isDanger: true,
|
| 150 |
onConfirm: async () => {
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
const updated = await api.schedules.get({ className: homeroomClass });
|
| 153 |
setSchedules(updated);
|
| 154 |
}
|
|
@@ -336,11 +359,14 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 336 |
<div className="text-[10px] text-gray-400 font-normal mt-1">{pConfig.startTime ? `${pConfig.startTime}-${pConfig.endTime}` : ''}</div>
|
| 337 |
</td>
|
| 338 |
{[1,2,3,4,5].map(day => {
|
|
|
|
| 339 |
const slotItems = schedules.filter(s =>
|
| 340 |
s.dayOfWeek === day &&
|
| 341 |
-
s.period === pConfig.period
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
| 344 |
|
| 345 |
return (
|
| 346 |
<td key={day} className="border-b border-r p-1 align-top h-24 relative group hover:bg-blue-50/50 transition-colors cursor-pointer" onClick={() => handleCellClick(day, pConfig.period)}>
|
|
@@ -350,12 +376,12 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 350 |
key={item._id}
|
| 351 |
className="p-1.5 rounded-md text-xs border shadow-sm relative group/item transition-transform hover:scale-[1.02]"
|
| 352 |
style={{backgroundColor: stringToColor(item.subject), borderColor: 'rgba(0,0,0,0.05)'}}
|
| 353 |
-
onClick={(e) => { e.stopPropagation(); handleCellClick(day, pConfig.period); }}
|
| 354 |
>
|
| 355 |
<div className="font-bold text-gray-800 truncate">{item.subject}</div>
|
| 356 |
<div className="flex justify-between items-center text-[10px] text-gray-600 mt-0.5">
|
| 357 |
<span className="truncate max-w-[60px]">{item.teacherName}</span>
|
| 358 |
-
{item.weekType !== 'ALL' && <span className=
|
| 359 |
</div>
|
| 360 |
<button
|
| 361 |
onClick={(e) => { e.stopPropagation(); handleDeleteSchedule(item); }}
|
|
@@ -383,7 +409,7 @@ export const TeacherDashboard: React.FC = () => {
|
|
| 383 |
{editingCell && (
|
| 384 |
<div className="fixed inset-0 bg-black/30 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
|
| 385 |
<div className="bg-white p-6 rounded-xl shadow-2xl w-full max-w-sm animate-in zoom-in-95">
|
| 386 |
-
<h4 className="font-bold mb-4 text-gray-800 text-lg border-b pb-2"
|
| 387 |
<div className="space-y-4">
|
| 388 |
<div>
|
| 389 |
<label className="text-xs font-bold text-gray-500 uppercase block mb-1">科目</label>
|
|
|
|
| 44 |
|
| 45 |
// Modals & Editing
|
| 46 |
const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
|
| 47 |
+
const [editForm, setEditForm] = useState<{ _id?: string, subject: string, teacherName: string, weekType: string }>({ subject: '', teacherName: '', weekType: 'ALL' });
|
| 48 |
const [isPeriodSettingsOpen, setIsPeriodSettingsOpen] = useState(false);
|
| 49 |
const [tempPeriodConfig, setTempPeriodConfig] = useState<PeriodConfig[]>([]);
|
| 50 |
|
|
|
|
| 103 |
finally { setLoading(false); }
|
| 104 |
};
|
| 105 |
|
| 106 |
+
const handleCellClick = (day: number, period: number, specificSchedule?: Schedule) => {
|
| 107 |
+
setEditingCell({ day, period });
|
| 108 |
+
|
| 109 |
+
if (specificSchedule) {
|
| 110 |
+
// Edit existing specific schedule
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
setEditForm({
|
| 112 |
+
_id: specificSchedule._id,
|
| 113 |
+
subject: specificSchedule.subject,
|
| 114 |
+
teacherName: specificSchedule.teacherName,
|
| 115 |
+
weekType: specificSchedule.weekType || 'ALL'
|
| 116 |
});
|
| 117 |
} else {
|
| 118 |
+
// Add new schedule (default to current view mode, or ALL if in ALL view)
|
| 119 |
+
setEditForm({
|
| 120 |
+
subject: '',
|
| 121 |
+
teacherName: '',
|
| 122 |
+
weekType: weekType === 'ALL' ? 'ALL' : weekType
|
| 123 |
+
});
|
| 124 |
}
|
|
|
|
| 125 |
};
|
| 126 |
|
| 127 |
const handleSaveSchedule = async () => {
|
| 128 |
if (!homeroomClass || !editingCell) return;
|
| 129 |
if (!editForm.subject) return alert('请选择科目');
|
| 130 |
+
|
| 131 |
try {
|
| 132 |
+
const payload = {
|
| 133 |
className: homeroomClass,
|
| 134 |
dayOfWeek: editingCell.day,
|
| 135 |
period: editingCell.period,
|
| 136 |
subject: editForm.subject,
|
| 137 |
teacherName: editForm.teacherName,
|
| 138 |
weekType: editForm.weekType as any
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
if (editForm._id) {
|
| 142 |
+
// Update existing by ID
|
| 143 |
+
await fetch(`/api/schedules/${editForm._id}`, {
|
| 144 |
+
method: 'PUT',
|
| 145 |
+
headers: {
|
| 146 |
+
'Content-Type': 'application/json',
|
| 147 |
+
'x-school-id': localStorage.getItem('admin_view_school_id') || ''
|
| 148 |
+
},
|
| 149 |
+
body: JSON.stringify(payload)
|
| 150 |
+
});
|
| 151 |
+
} else {
|
| 152 |
+
// Create new
|
| 153 |
+
await api.schedules.save(payload);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
setEditingCell(null);
|
| 157 |
const updated = await api.schedules.get({ className: homeroomClass });
|
| 158 |
setSchedules(updated);
|
|
|
|
| 163 |
setConfirmModal({
|
| 164 |
isOpen: true,
|
| 165 |
title: '删除课程',
|
| 166 |
+
message: `确定要删除 ${s.subject} (${s.weekType==='ODD'?'单周':s.weekType==='EVEN'?'双周':'全周'}) 吗?`,
|
| 167 |
isDanger: true,
|
| 168 |
onConfirm: async () => {
|
| 169 |
+
// Use ID based delete if available, otherwise fallback to old query
|
| 170 |
+
if (s._id) {
|
| 171 |
+
await fetch(`/api/schedules?id=${s._id}`, { method: 'DELETE', headers: { 'x-school-id': localStorage.getItem('admin_view_school_id') || '' } });
|
| 172 |
+
} else {
|
| 173 |
+
await api.schedules.delete({ className: s.className, dayOfWeek: s.dayOfWeek, period: s.period });
|
| 174 |
+
}
|
| 175 |
const updated = await api.schedules.get({ className: homeroomClass });
|
| 176 |
setSchedules(updated);
|
| 177 |
}
|
|
|
|
| 359 |
<div className="text-[10px] text-gray-400 font-normal mt-1">{pConfig.startTime ? `${pConfig.startTime}-${pConfig.endTime}` : ''}</div>
|
| 360 |
</td>
|
| 361 |
{[1,2,3,4,5].map(day => {
|
| 362 |
+
// Enhanced filter: support displaying simultaneous odd/even records
|
| 363 |
const slotItems = schedules.filter(s =>
|
| 364 |
s.dayOfWeek === day &&
|
| 365 |
+
s.period === pConfig.period
|
| 366 |
+
).filter(s => {
|
| 367 |
+
if (weekType === 'ALL') return true;
|
| 368 |
+
return s.weekType === 'ALL' || s.weekType === weekType;
|
| 369 |
+
});
|
| 370 |
|
| 371 |
return (
|
| 372 |
<td key={day} className="border-b border-r p-1 align-top h-24 relative group hover:bg-blue-50/50 transition-colors cursor-pointer" onClick={() => handleCellClick(day, pConfig.period)}>
|
|
|
|
| 376 |
key={item._id}
|
| 377 |
className="p-1.5 rounded-md text-xs border shadow-sm relative group/item transition-transform hover:scale-[1.02]"
|
| 378 |
style={{backgroundColor: stringToColor(item.subject), borderColor: 'rgba(0,0,0,0.05)'}}
|
| 379 |
+
onClick={(e) => { e.stopPropagation(); handleCellClick(day, pConfig.period, item); }}
|
| 380 |
>
|
| 381 |
<div className="font-bold text-gray-800 truncate">{item.subject}</div>
|
| 382 |
<div className="flex justify-between items-center text-[10px] text-gray-600 mt-0.5">
|
| 383 |
<span className="truncate max-w-[60px]">{item.teacherName}</span>
|
| 384 |
+
{item.weekType !== 'ALL' && <span className={`px-1 rounded text-[9px] font-bold border border-black/5 ${item.weekType==='ODD'?'bg-blue-100 text-blue-700':'bg-green-100 text-green-700'}`}>{item.weekType==='ODD'?'单':'双'}</span>}
|
| 385 |
</div>
|
| 386 |
<button
|
| 387 |
onClick={(e) => { e.stopPropagation(); handleDeleteSchedule(item); }}
|
|
|
|
| 409 |
{editingCell && (
|
| 410 |
<div className="fixed inset-0 bg-black/30 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
|
| 411 |
<div className="bg-white p-6 rounded-xl shadow-2xl w-full max-w-sm animate-in zoom-in-95">
|
| 412 |
+
<h4 className="font-bold mb-4 text-gray-800 text-lg border-b pb-2">{editForm._id ? '编辑课程信息' : '新增课程信息'}</h4>
|
| 413 |
<div className="space-y-4">
|
| 414 |
<div>
|
| 415 |
<label className="text-xs font-bold text-gray-500 uppercase block mb-1">科目</label>
|
server.js
CHANGED
|
@@ -47,6 +47,15 @@ const connectDB = async () => {
|
|
| 47 |
try {
|
| 48 |
await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
|
| 49 |
console.log('✅ MongoDB 连接成功 (Real Data)');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
} catch (err) {
|
| 51 |
console.error('❌ MongoDB 连接失败:', err.message);
|
| 52 |
InMemoryDB.isFallback = true;
|
|
@@ -140,12 +149,26 @@ app.get('/api/schedules', async (req, res) => {
|
|
| 140 |
res.json(await ScheduleModel.find(query));
|
| 141 |
});
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
app.post('/api/schedules', async (req, res) => {
|
| 144 |
try {
|
| 145 |
-
//
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
| 149 |
const sId = req.headers['x-school-id'];
|
| 150 |
if(sId) filter.schoolId = sId;
|
| 151 |
|
|
@@ -159,7 +182,12 @@ app.post('/api/schedules', async (req, res) => {
|
|
| 159 |
|
| 160 |
app.delete('/api/schedules', async (req, res) => {
|
| 161 |
try {
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
res.json({});
|
| 164 |
} catch (e) {
|
| 165 |
res.status(500).json({ error: e.message });
|
|
@@ -173,7 +201,7 @@ app.put('/api/users/:id/menu-order', async (req, res) => {
|
|
| 173 |
res.json({ success: true });
|
| 174 |
});
|
| 175 |
|
| 176 |
-
//
|
| 177 |
app.get('/api/classes/:className/teachers', async (req, res) => {
|
| 178 |
const { className } = req.params;
|
| 179 |
const schoolId = req.headers['x-school-id'];
|
|
|
|
| 47 |
try {
|
| 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');
|
| 55 |
+
} catch (e) {
|
| 56 |
+
// Ignore error if index doesn't exist
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
} catch (err) {
|
| 60 |
console.error('❌ MongoDB 连接失败:', err.message);
|
| 61 |
InMemoryDB.isFallback = true;
|
|
|
|
| 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);
|
| 156 |
+
res.json({ success: true });
|
| 157 |
+
} catch (e) {
|
| 158 |
+
res.status(500).json({ error: e.message });
|
| 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,
|
| 169 |
+
period: req.body.period,
|
| 170 |
+
weekType: req.body.weekType || 'ALL'
|
| 171 |
+
};
|
| 172 |
const sId = req.headers['x-school-id'];
|
| 173 |
if(sId) filter.schoolId = sId;
|
| 174 |
|
|
|
|
| 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 {
|
| 189 |
+
await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query});
|
| 190 |
+
}
|
| 191 |
res.json({});
|
| 192 |
} catch (e) {
|
| 193 |
res.status(500).json({ error: e.message });
|
|
|
|
| 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'];
|