import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Avatar, Button, Drawer, Empty, Input, Progress, Select, Table, Tag } from 'antd'; import dayjs from 'dayjs'; import api from '../../api'; import { message } from '../../ui/app-message'; import { filterLessonAttendanceRecords, getPunchDisplayInfo, summarizeLessonCheckins, type LessonAttendanceFilter, } from './attendance-workspace'; import type { LessonAttendanceRecord, LessonAttendanceSchedule } from './types'; interface LessonAttendanceSession { id: number; scheduleId: number; classId: number; lessonDate: string; status: 'in_progress' | 'completed'; } interface LessonAttendanceResponse { schedule: LessonAttendanceSchedule; session: LessonAttendanceSession | null; records: LessonAttendanceRecord[]; } interface LessonAttendanceDetailProps { schedule: LessonAttendanceSchedule | null; className: string; onClose: () => void; } function AttendanceStatus({ status }: { status: string }) { const checkedIn = status === 'present' || status === 'late'; return ( {checkedIn ? '出勤' : '缺勤'} ); } function LessonSummary({ records }: { records: readonly LessonAttendanceRecord[] }) { const summary = summarizeLessonCheckins(records); const rate = summary.total > 0 ? Math.round((summary.checkedIn / summary.total) * 100) : 0; return (
打卡率 {summary.total} 人
{summary.checkedIn} 已打卡
{summary.notCheckedIn} 未打卡
); } const LessonAttendanceDetail: React.FC = ({ schedule, className, onClose, }) => { const [loadedSchedule, setLoadedSchedule] = useState(null); const [session, setSession] = useState(null); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [keyword, setKeyword] = useState(''); const [filter, setFilter] = useState('all'); useEffect(() => { if (!schedule) return; let cancelled = false; setLoadedSchedule(schedule); setSession(null); setRecords([]); setKeyword(''); setFilter('all'); setLoading(true); const date = dayjs().format('YYYY-MM-DD'); void api .post(`/attendance-lessons/schedules/${schedule.id}/pull`, { date, }) .then((data) => { if (cancelled) return; setLoadedSchedule(data.schedule); setSession(data.session); setRecords(data.records); message.success('钉钉打卡已更新;课程截止后系统将自动结算'); }) .catch((error: unknown) => { if (cancelled) return; message.error((error as { message?: string })?.message || '加载本节课考勤失败'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [schedule]); const updateRecord = useCallback(async (record: LessonAttendanceRecord, status: string) => { const previous = record.status; setRecords((items) => items.map((item) => (item.id === record.id ? { ...item, status } : item)), ); try { await api.put(`/attendance-records/${record.id}`, { status }); } catch (error: unknown) { setRecords((items) => items.map((item) => (item.id === record.id ? { ...item, status: previous } : item)), ); message.error((error as { message?: string })?.message || '更新考勤失败'); } }, []); const filteredRecords = useMemo( () => filterLessonAttendanceRecords(records, keyword, filter), [filter, keyword, records], ); const completed = session?.status === 'completed'; const displayedSchedule = loadedSchedule ?? schedule; return (
LESSON ATTENDANCE

{displayedSchedule?.subject || '课程考勤'}

{className} · {displayedSchedule?.startTime}–{displayedSchedule?.endTime} ·{' '} {dayjs().format('YYYY-MM-DD')}

{session && ( )}
setKeyword(event.target.value)} className="lesson-record-search" /> value={filter} onChange={setFilter} options={[ { value: 'all', label: '全部学生' }, { value: 'checked_in', label: '已打卡' }, { value: 'not_checked_in', label: '未打卡' }, ]} className="lesson-record-filter-select" /> 显示 {filteredRecords.length} / {records.length} 人
rowKey="id" loading={loading} dataSource={filteredRecords} pagination={false} locale={{ emptyText: ( ), }} columns={[ { title: '学生', dataIndex: ['student', 'name'], render: (name: string) => (
{name?.slice(0, 1)} {name || '-'}
), }, { title: '考勤结果', dataIndex: 'status', width: 230, render: (value: string, record) => { const checkedIn = value === 'present' || value === 'late'; return (
); }, }, { title: '当前状态', dataIndex: 'status', width: 105, render: (value: string) => , }, { title: '打卡设备', width: 220, render: (_: unknown, record) => { const info = getPunchDisplayInfo(record); if (!info) return ; return (
{info.label} {info.detail && {info.detail}} {info.time && {dayjs(info.time).format('HH:mm:ss')}}
); }, }, { title: '备注', dataIndex: 'remark', render: (value: string | null) => value || , }, ]} />
); }; export default LessonAttendanceDetail;