Spaces:
Sleeping
Sleeping
Upload 53 files
Browse files- components/ConfirmModal.tsx +1 -1
- pages/AchievementTeacher.tsx +4 -3
- pages/Attendance.tsx +53 -36
components/ConfirmModal.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|
| 22 |
|
| 23 |
const handleConfirm = () => {
|
| 24 |
onConfirm();
|
| 25 |
-
onClose();
|
| 26 |
};
|
| 27 |
|
| 28 |
return (
|
|
|
|
| 22 |
|
| 23 |
const handleConfirm = () => {
|
| 24 |
onConfirm();
|
| 25 |
+
onClose();
|
| 26 |
};
|
| 27 |
|
| 28 |
return (
|
pages/AchievementTeacher.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { AchievementConfig, AchievementItem, ExchangeRule, Student, TeacherExcha
|
|
| 5 |
import { Plus, Trash2, Edit, Save, Gift, Award, Coins, Users, Search, Loader2, CheckCircle, Filter } from 'lucide-react';
|
| 6 |
import { Emoji } from '../components/Emoji';
|
| 7 |
import { ConfirmModal } from '../components/ConfirmModal';
|
|
|
|
| 8 |
|
| 9 |
const PRESET_ICONS = [
|
| 10 |
{ icon: '🏆', label: '冠军杯' }, { icon: '🥇', label: '金牌' }, { icon: '🥈', label: '银牌' }, { icon: '🥉', label: '铜牌' },
|
|
@@ -161,7 +162,7 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
|
|
| 161 |
<div className="flex justify-end items-center gap-2"><span className="text-xs font-bold text-gray-500"><Filter size={14} className="inline mr-1"/>筛选添加者:</span><select className="border rounded text-xs p-1" value={filterCreator} onChange={e=>setFilterCreator(e.target.value)}><option value="ALL">全部老师</option>{uniqueCreators.map(c => <option key={c} value={c}>{c}</option>)}</select></div>
|
| 162 |
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100 flex flex-col md:flex-row gap-4 items-end">
|
| 163 |
<div className="flex-1 w-full"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">成就名称</label><input className="w-full border rounded p-2 text-sm" placeholder="如: 劳动小能手" value={newAchieve.name} onChange={e => setNewAchieve({...newAchieve, name: e.target.value})} /></div>
|
| 164 |
-
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">分值 (小红花)</label><
|
| 165 |
<div className="w-full md:w-auto"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">图标预设</label><select className="w-full border rounded p-2 min-w-[100px] text-sm bg-white" value={newAchieve.icon} onChange={e => setNewAchieve({...newAchieve, icon: e.target.value})}>{PRESET_ICONS.map(p => <option key={p.label} value={p.icon}>{p.icon} {p.label}</option>)}</select></div>
|
| 166 |
<button onClick={addAchievement} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-blue-700 whitespace-nowrap text-sm">添加成就</button>
|
| 167 |
</div>
|
|
@@ -223,10 +224,10 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
|
|
| 223 |
<div className="space-y-6 max-w-4xl mx-auto">
|
| 224 |
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100 text-sm text-purple-800 mb-4"><span className="font-bold">提示:</span> 这里的兑换规则是您个人专属的,对您教的所有班级学生可见。学生兑换后,只有您能看到待处理的申请。</div>
|
| 225 |
<div className="bg-green-50 p-4 rounded-xl border border-green-100 flex flex-col md:flex-row gap-4 items-end">
|
| 226 |
-
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">消耗小红花</label><
|
| 227 |
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励类型</label><select className="w-full border rounded p-2 text-sm bg-white" value={newRule.rewardType} onChange={e => setNewRule({...newRule, rewardType: e.target.value as any})}><option value="DRAW_COUNT">🎲 抽奖券</option><option value="ITEM">🎁 实物/特权</option></select></div>
|
| 228 |
<div className="flex-1 w-full"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励名称</label><input className="w-full border rounded p-2 text-sm" placeholder="如: 免作业券" value={newRule.rewardName} onChange={e => setNewRule({...newRule, rewardName: e.target.value})}/></div>
|
| 229 |
-
<div className="w-24"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">数量</label><
|
| 230 |
<button onClick={addRule} className="bg-green-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-green-700 whitespace-nowrap text-sm">添加规则</button>
|
| 231 |
</div>
|
| 232 |
<div className="space-y-3">{myRules.length === 0 ? <div className="text-center text-gray-400 py-4">暂无个人兑换规则</div> : myRules.map(rule => (
|
|
|
|
| 5 |
import { Plus, Trash2, Edit, Save, Gift, Award, Coins, Users, Search, Loader2, CheckCircle, Filter } from 'lucide-react';
|
| 6 |
import { Emoji } from '../components/Emoji';
|
| 7 |
import { ConfirmModal } from '../components/ConfirmModal';
|
| 8 |
+
import { NumberInput } from '../components/NumberInput';
|
| 9 |
|
| 10 |
const PRESET_ICONS = [
|
| 11 |
{ icon: '🏆', label: '冠军杯' }, { icon: '🥇', label: '金牌' }, { icon: '🥈', label: '银牌' }, { icon: '🥉', label: '铜牌' },
|
|
|
|
| 162 |
<div className="flex justify-end items-center gap-2"><span className="text-xs font-bold text-gray-500"><Filter size={14} className="inline mr-1"/>筛选添加者:</span><select className="border rounded text-xs p-1" value={filterCreator} onChange={e=>setFilterCreator(e.target.value)}><option value="ALL">全部老师</option>{uniqueCreators.map(c => <option key={c} value={c}>{c}</option>)}</select></div>
|
| 163 |
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100 flex flex-col md:flex-row gap-4 items-end">
|
| 164 |
<div className="flex-1 w-full"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">成就名称</label><input className="w-full border rounded p-2 text-sm" placeholder="如: 劳动小能手" value={newAchieve.name} onChange={e => setNewAchieve({...newAchieve, name: e.target.value})} /></div>
|
| 165 |
+
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">分值 (小红花)</label><NumberInput min={1} className="w-full" value={newAchieve.points} onChange={v => setNewAchieve({...newAchieve, points: v})} /></div>
|
| 166 |
<div className="w-full md:w-auto"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">图标预设</label><select className="w-full border rounded p-2 min-w-[100px] text-sm bg-white" value={newAchieve.icon} onChange={e => setNewAchieve({...newAchieve, icon: e.target.value})}>{PRESET_ICONS.map(p => <option key={p.label} value={p.icon}>{p.icon} {p.label}</option>)}</select></div>
|
| 167 |
<button onClick={addAchievement} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-blue-700 whitespace-nowrap text-sm">添加成就</button>
|
| 168 |
</div>
|
|
|
|
| 224 |
<div className="space-y-6 max-w-4xl mx-auto">
|
| 225 |
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100 text-sm text-purple-800 mb-4"><span className="font-bold">提示:</span> 这里的兑换规则是您个人专属的,对您教的所有班级学生可见。学生兑换后,只有您能看到待处理的申请。</div>
|
| 226 |
<div className="bg-green-50 p-4 rounded-xl border border-green-100 flex flex-col md:flex-row gap-4 items-end">
|
| 227 |
+
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">消耗小红花</label><NumberInput min={1} className="w-full" value={newRule.cost} onChange={v => setNewRule({...newRule,cost: v})}/></div>
|
| 228 |
<div className="w-full md:w-32"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励类型</label><select className="w-full border rounded p-2 text-sm bg-white" value={newRule.rewardType} onChange={e => setNewRule({...newRule, rewardType: e.target.value as any})}><option value="DRAW_COUNT">🎲 抽奖券</option><option value="ITEM">🎁 实物/特权</option></select></div>
|
| 229 |
<div className="flex-1 w-full"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励名称</label><input className="w-full border rounded p-2 text-sm" placeholder="如: 免作业券" value={newRule.rewardName} onChange={e => setNewRule({...newRule, rewardName: e.target.value})}/></div>
|
| 230 |
+
<div className="w-24"><label className="text-xs font-bold text-gray-500 uppercase mb-1 block">数量</label><NumberInput min={1} className="w-full" value={newRule.rewardValue} onChange={v => setNewRule({...newRule, rewardValue: v})}/></div>
|
| 231 |
<button onClick={addRule} className="bg-green-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-green-700 whitespace-nowrap text-sm">添加规则</button>
|
| 232 |
</div>
|
| 233 |
<div className="space-y-3">{myRules.length === 0 ? <div className="text-center text-gray-400 py-4">暂无个人兑换规则</div> : myRules.map(rule => (
|
pages/Attendance.tsx
CHANGED
|
@@ -149,45 +149,62 @@ export const AttendancePage: React.FC = () => {
|
|
| 149 |
});
|
| 150 |
};
|
| 151 |
|
| 152 |
-
// Toggle Day Type
|
| 153 |
const toggleDayType = async () => {
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
loadData();
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
-
}
|
| 190 |
-
}
|
| 191 |
};
|
| 192 |
|
| 193 |
if (!targetClass) return <div className="p-10 text-center text-gray-500">您不是班主任,无法管理考勤。</div>;
|
|
|
|
| 149 |
});
|
| 150 |
};
|
| 151 |
|
| 152 |
+
// Improved Logic: Smart Toggle Day Type
|
| 153 |
const toggleDayType = async () => {
|
| 154 |
+
const isCurrentlyHoliday = isHoliday; // Based on calculation
|
| 155 |
+
const targetIsWorkday = isCurrentlyHoliday; // If currently holiday, we want to make it workday
|
| 156 |
+
|
| 157 |
+
const confirmMsg = targetIsWorkday
|
| 158 |
+
? `确定将 ${date} 设为“工作日”并开启考勤吗?`
|
| 159 |
+
: `确定将 ${date} 标记为“休息日”并免除考勤吗?`;
|
| 160 |
+
|
| 161 |
+
setConfirmModal({
|
| 162 |
+
isOpen: true,
|
| 163 |
+
title: targetIsWorkday ? '开启考勤' : '关闭考勤',
|
| 164 |
+
message: confirmMsg,
|
| 165 |
+
onConfirm: async () => {
|
| 166 |
+
try {
|
| 167 |
+
// 1. Find overlapping single-day entries and delete them
|
| 168 |
+
const overlapping = calendarEntries.filter(e => e.startDate === date && e.endDate === date);
|
| 169 |
+
for (const entry of overlapping) {
|
| 170 |
+
if (entry._id) await api.calendar.delete(entry._id);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// 2. Check default state (Weekend vs Weekday)
|
| 174 |
+
const dayOfWeek = new Date(date).getDay();
|
| 175 |
+
const isDefaultHoliday = excludeWeekends && (dayOfWeek === 0 || dayOfWeek === 6);
|
| 176 |
+
|
| 177 |
+
// 3. Add new entry ONLY if target state differs from default state
|
| 178 |
+
if (targetIsWorkday && isDefaultHoliday) {
|
| 179 |
+
// It's a weekend, but we want Workday -> Add WORKDAY exception
|
| 180 |
+
await api.calendar.add({
|
| 181 |
+
schoolId: currentUser?.schoolId!,
|
| 182 |
+
className: targetClass,
|
| 183 |
+
type: 'WORKDAY',
|
| 184 |
+
startDate: date,
|
| 185 |
+
endDate: date,
|
| 186 |
+
name: '补班/工作日'
|
| 187 |
+
});
|
| 188 |
+
} else if (!targetIsWorkday && !isDefaultHoliday) {
|
| 189 |
+
// It's a weekday, but we want Holiday -> Add OFF exception
|
| 190 |
+
await api.calendar.add({
|
| 191 |
+
schoolId: currentUser?.schoolId!,
|
| 192 |
+
className: targetClass,
|
| 193 |
+
type: 'OFF',
|
| 194 |
+
startDate: date,
|
| 195 |
+
endDate: date,
|
| 196 |
+
name: '休息/停课'
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
// If target state matches default (e.g. making Monday a Workday), we just deleted the 'OFF' exception above, so it reverts to default Workday.
|
| 200 |
+
|
| 201 |
+
setToast({ show: true, message: '日程已更新', type: 'success' });
|
| 202 |
loadData();
|
| 203 |
+
} catch (e) {
|
| 204 |
+
setToast({ show: true, message: '操作失败', type: 'error' });
|
| 205 |
}
|
| 206 |
+
}
|
| 207 |
+
});
|
| 208 |
};
|
| 209 |
|
| 210 |
if (!targetClass) return <div className="p-10 text-center text-gray-500">您不是班主任,无法管理考勤。</div>;
|