dvc890 commited on
Commit
74eefb3
·
verified ·
1 Parent(s): 3fb8bc3

Upload 51 files

Browse files
Files changed (2) hide show
  1. pages/TeacherDashboard.tsx +51 -25
  2. 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
- // Find existing schedule for this slot
108
- const existingSlot = schedules.find(s =>
109
- s.dayOfWeek === day &&
110
- s.period === period &&
111
- (s.weekType === 'ALL' || s.weekType === weekType || weekType === 'ALL')
112
- );
113
-
114
- if (existingSlot) {
115
  setEditForm({
116
- subject: existingSlot.subject,
117
- teacherName: existingSlot.teacherName,
118
- weekType: existingSlot.weekType || 'ALL'
 
119
  });
120
  } else {
121
- setEditForm({ subject: '', teacherName: '', weekType: weekType === 'ALL' ? 'ALL' : weekType });
 
 
 
 
 
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
- await api.schedules.save({
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
- await api.schedules.delete({ className: s.className, dayOfWeek: s.dayOfWeek, period: s.period });
 
 
 
 
 
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
- (s.weekType === 'ALL' || s.weekType === weekType || weekType === 'ALL')
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="bg-white/60 px-1 rounded text-[9px] font-bold border border-black/5">{item.weekType==='ODD'?'单':'双'}</span>}
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">编辑课程信息</h4>
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
- // FIX: Removed weekType from filter to prevent Duplicate Key Error on existing unique index.
146
- // The DB index is (schoolId, className, dayOfWeek, period).
147
- // This means we can only have one schedule per slot. Updating weekType should update the existing slot.
148
- const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period };
 
 
 
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
- await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query});
 
 
 
 
 
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
- // --- Existing Routes (Minimally Modified) ---
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'];