diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 2b570cb..02efee7 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -82,6 +82,14 @@ export const hoursApi = { batch: (activity_id: number, items: Array<{ user_id: number; hours: number; remark: string }>) => api.post('/hours/batch', { activity_id, items }).then(r => r.data), byActivity: (id: number) => api.get(`/hours/activity/${id}`).then(r => r.data), + directAdd: (items: Array<{ user_id: number; hours: number; reason: string }>) => + api.post<{ + ok: boolean; + added: number; + total_hours: number; + results: Array<{ user_id: number; class: string; name: string; volunteer_no: string | null; hours: number; reason: string }>; + direct_add_activity_id: number; + }>('/hours/direct-add', { items }).then(r => r.data), }; export const exportApi = { diff --git a/client/src/pages/admin/Users.tsx b/client/src/pages/admin/Users.tsx index fb65350..d0b9d3d 100644 --- a/client/src/pages/admin/Users.tsx +++ b/client/src/pages/admin/Users.tsx @@ -4,7 +4,7 @@ import Input from '../../components/Input'; import DataTable from '../../components/DataTable'; import Modal from '../../components/Modal'; import Textarea from '../../components/Textarea'; -import { groupsApi, usersApi, errMsg } from '../../api'; +import { groupsApi, usersApi, hoursApi, errMsg } from '../../api'; import { formatDateRange } from '../../utils/export'; import type { UserItem, UserDetail, WorkGroup } from '../../types'; import { IconUsers, IconPlus, IconEdit, IconTrash, IconEye } from '../../components/Icons'; @@ -105,6 +105,13 @@ export default function Users() { const [selectedIds, setSelectedIds] = useState>(new Set()); + const [directOpen, setDirectOpen] = useState(false); + const [directUserIds, setDirectUserIds] = useState([]); + const [directHours, setDirectHours] = useState(''); + const [directReason, setDirectReason] = useState(''); + const [directError, setDirectError] = useState(''); + const [directSubmitting, setDirectSubmitting] = useState(false); + const load = async () => { setLoading(true); try { @@ -208,6 +215,62 @@ export default function Users() { } }; + const openBatchDirectAdd = () => { + if (selectedCount === 0) return; + setDirectUserIds(Array.from(selectedIds)); + setDirectHours(''); + setDirectReason(''); + setDirectError(''); + setDirectOpen(true); + }; + const openSingleDirectAdd = (u: { id: number }) => { + setDirectUserIds([u.id]); + setDirectHours(''); + setDirectReason(''); + setDirectError(''); + setDirectOpen(true); + }; + const refreshDetailAfterDirect = async () => { + if (!detailOpen || !detail) return; + try { + const d = await usersApi.detail(detail.id); + setDetail({ ...d, group_ids: d.group_ids ?? [], group_names: d.group_names ?? [] }); + } catch {} + }; + const submitDirectAdd = async () => { + setDirectError(''); + const h = Number(directHours); + const reason = directReason.trim(); + if (!Number.isFinite(h) || h <= 0) { + setDirectError('时长必须是大于 0 的数字'); + return; + } + if (!reason) { + setDirectError('必须填写原因(将作为备注保存)'); + return; + } + if (directUserIds.length === 0) { + setDirectError('请先选择部员'); + return; + } + setDirectSubmitting(true); + try { + const items = directUserIds.map((id) => ({ user_id: id, hours: h, reason })); + const result = await hoursApi.directAdd(items); + const names = result.results.slice(0, 5).map((r) => `${r.class}${r.name}`).join('、'); + const more = result.results.length > 5 ? ` 等 ${result.results.length} 人` : ''; + alert(`补录完成:已为 ${names}${more} 累计加 ${Number(result.total_hours).toFixed(1)} 小时`); + setDirectOpen(false); + clearSelection(); + await load(); + await refreshDetailAfterDirect(); + } catch (e) { + setDirectError(errMsg(e, '补录义工时失败')); + } finally { + setDirectSubmitting(false); + } + }; + const submit = async () => { if (!form.class.trim() || !form.name.trim()) { setError('班级与姓名不能为空'); @@ -402,11 +465,18 @@ export default function Users() { />
{selectedCount > 0 && ( - + <> + + + )}
-
-
累计时长
-
- {Number(detail.total_hours || 0).toFixed(1)}h +
+
+
累计时长
+
+ {Number(detail.total_hours || 0).toFixed(1)}h +
+
@@ -692,6 +769,86 @@ export default function Users() {
)} + + !directSubmitting && setDirectOpen(false)} + footer={ + <> + + + + } + > +
+
+
+
说明
+
+ · 录入后会作为独立的时长记录挂在虚拟活动「【补录】直接加义工时长」名下
+ · 每次直接加时都会单独保存一条完整记录(含原因),可在明细里逐条追溯
+ · 「原因」将作为该条时长的备注显示 +
+
+
+ 已选择 {directUserIds.length} 名部员: +
+
+ {(() => { + const namesMap = new Map(); + for (const u of list) namesMap.set(u.id, `${u.class}${u.name}`); + return directUserIds.map((id, i) => ( + + {i + 1}. {namesMap.get(id) || `部员ID#${id}`} + + )); + })()} +
+
+
+
+
+ + setDirectHours(e.target.value)} + placeholder="请输入要加的义工小时数(必填,> 0)" + /> +
+
+ +