import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ATTENDANCE_STATUS_OPTIONS, apiFetchAttendance, apiFetchAttendanceShifts, apiPushAttendanceQLDT, apiUpdateAttendanceStatus, apiUpdateAttendanceBulkStatus, attendanceStatusClass, type AttendanceRow, type AttendanceShiftInfo, apiFetchLeaveRequests, apiUpdateLeaveStatus, type LeaveRequestItem, } from '../api'; interface AttendancePanelProps { classId: number; } function formatQldtTime(iso?: string): string { if (!iso) return ''; return new Date(iso).toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); } export const AttendancePanel: React.FC = ({ classId }) => { const today = new Date().toISOString().slice(0, 10); const [date, setDate] = useState(today); const [period, setPeriod] = useState(1); const [rows, setRows] = useState([]); const [shifts, setShifts] = useState([]); const [shiftInfo, setShiftInfo] = useState(null); const [loading, setLoading] = useState(false); const [pushing, setPushing] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [selectedStudentRkIds, setSelectedStudentRkIds] = useState([]); const [isCollapsed, setIsCollapsed] = useState(false); const [leaveRequests, setLeaveRequests] = useState([]); const [loadingLeave, setLoadingLeave] = useState(false); const [showLeaveModal, setShowLeaveModal] = useState(false); const handleScroll = useCallback((e: React.UIEvent) => { const scrollTop = e.currentTarget.scrollTop; if (scrollTop > 20) { setIsCollapsed(true); } else if (scrollTop <= 5) { setIsCollapsed(false); } }, []); const loadShifts = useCallback(async () => { try { const res = await apiFetchAttendanceShifts(classId, date); setShifts(res.data || []); if (res.data?.length && !res.data.find((s: any) => s.period === period)) { setPeriod(res.data[0].period || 1); } } catch { setShifts([]); } }, [classId, date, period]); const loadAttendance = useCallback(async (isBackground: boolean | any = false) => { const isBg = isBackground === true; try { if (!isBg) setLoading(true); const res = await apiFetchAttendance(classId, date, period); setRows(res.data || []); setShiftInfo(res.shift || null); if (!isBg) setSelectedStudentRkIds([]); // Clear selection when data changes } catch (e: any) { alert(e.message || 'Không tải được điểm danh'); } finally { if (!isBg) setLoading(false); } }, [classId, date, period]); const currentShift = useMemo(() => { return shifts.find(s => s.period === period); }, [shifts, period]); const courseId = currentShift?.courseId; const loadLeaveRequests = useCallback(async () => { if (!courseId) { setLeaveRequests([]); return; } try { setLoadingLeave(true); const res = await apiFetchLeaveRequests(classId, courseId, date); setLeaveRequests(res || []); } catch { setLeaveRequests([]); } finally { setLoadingLeave(false); } }, [classId, courseId, date]); useEffect(() => { loadShifts(); }, [loadShifts]); useEffect(() => { loadAttendance(); }, [loadAttendance]); useEffect(() => { loadLeaveRequests(); }, [loadLeaveRequests]); const handleUpdateLeaveStatus = async (leaveId: number, status: string, studentRkId: number) => { try { await apiUpdateLeaveStatus(classId, leaveId, { status, studentRkId, date, period, }); await loadAttendance(); await loadLeaveRequests(); } catch (e: any) { alert(e.message || 'Cập nhật đơn xin nghỉ thất bại'); } }; const filteredRows = useMemo(() => { const q = searchQuery.trim().toLowerCase(); if (!q) return rows; return rows.filter(row => { const haystack = [row.fullName, row.studentCode, row.email, row.statusLabel] .filter(Boolean) .join(' ') .toLowerCase(); return haystack.includes(q); }); }, [rows, searchQuery]); const statusCounts = useMemo(() => { const counts: Record = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; for (const row of rows) { if (counts[row.status] !== undefined) counts[row.status]++; } return counts; }, [rows]); const toggleStudentSelection = useCallback((studentRkId: number) => { setSelectedStudentRkIds(prev => prev.includes(studentRkId) ? prev.filter(id => id !== studentRkId) : [...prev, studentRkId] ); }, []); const handleStatusChange = async (studentRkId: number, status: number) => { // Optimistic state update so the UI reacts instantly setRows(prevRows => prevRows.map(row => { if (row.studentRkId === studentRkId) { const matchedOpt = ATTENDANCE_STATUS_OPTIONS.find(o => o.value === status); return { ...row, status, statusLabel: matchedOpt ? matchedOpt.label : row.statusLabel, statusEditedByTeacher: true, }; } return row; }) ); try { await apiUpdateAttendanceStatus(classId, { date, period, studentRkId, status }); await loadAttendance(true); } catch (e: any) { alert(e.message || 'Cập nhật trạng thái thất bại'); await loadAttendance(); // Rollback on failure } }; const handleBulkStatusChange = async (status: number) => { if (selectedStudentRkIds.length === 0) return; // Optimistic state update for selected students setRows(prevRows => prevRows.map(row => { if (selectedStudentRkIds.includes(row.studentRkId)) { const matchedOpt = ATTENDANCE_STATUS_OPTIONS.find(o => o.value === status); return { ...row, status, statusLabel: matchedOpt ? matchedOpt.label : row.statusLabel, statusEditedByTeacher: true, }; } return row; }) ); const idsToUpdate = [...selectedStudentRkIds]; setSelectedStudentRkIds([]); try { await apiUpdateAttendanceBulkStatus(classId, { date, period, studentRkIds: idsToUpdate, status, }); await loadAttendance(true); } catch (e: any) { alert(e.message || 'Cập nhật hàng loạt thất bại'); await loadAttendance(); // Rollback } }; const handlePushQLDT = async () => { if (!shiftInfo?.courseId) { alert('Ca học này chưa chọn môn học. Vui lòng cấu hình trong lịch học.'); return; } if (!confirm(`Đẩy điểm danh Ca ${period} ngày ${date} lên QLĐT?`)) return; try { setPushing(true); const res = await apiPushAttendanceQLDT(classId, date, period); alert(res.message || 'Đã đẩy lên QLĐT'); await loadAttendance(); } catch (e: any) { alert(e.message || 'Đẩy QLĐT thất bại'); } finally { setPushing(false); } }; const qldtSynced = Boolean(shiftInfo?.pushedToQldtAt); const qldtDirty = Boolean(shiftInfo?.qldtDirty); return (
{courseId && ( )}
{qldtSynced ? ( qldtDirty ? ( <> Đã lưu QLĐT — có thay đổi mới Lần đẩy: {formatQldtTime(shiftInfo?.pushedToQldtAt)} · Cần đẩy lại sau khi chỉnh sửa ) : ( <> Đã lưu QLĐT Lần đẩy gần nhất: {formatQldtTime(shiftInfo?.pushedToQldtAt)} ) ) : ( <> Chưa đẩy lên QLĐT Ca {period} ngày {date} — bấm "Đẩy QLĐT" sau khi kiểm tra trạng thái )}
{currentShift && (
{currentShift.startTime}–{currentShift.endTime} {currentShift.courseName || 'Chưa chọn môn'} {!currentShift.isActive && Ca tắt}
)}
{ATTENDANCE_STATUS_OPTIONS.map(opt => ( ))} {filteredRows.length}/{rows.length} SV {searchQuery.trim() ? ' (đã lọc)' : ''}

Sửa trạng thái thủ công sẽ được khóa — hệ thống tự tính sẽ không ghi đè.

💡 Lưu ý: Hệ thống hiện tại chỉ ghi nhận điểm danh học tập nội bộ và KHÔNG tự động lưu lên QLĐT. Thầy cô vui lòng kiểm tra kỹ trạng thái của sinh viên, sau đó bấm nút Đẩy QLĐT ở góc phải bên trên để đồng bộ điểm danh chính thức.
{/* Leave Requests Approval Modal */} {showLeaveModal && courseId && (
setShowLeaveModal(false)}>
e.stopPropagation()}>

✉️ Đơn xin nghỉ phép trong ca

Ca {period} ngày {date} - Giao diện duyệt đơn xin nghỉ từ cổng QLĐT portal.

{loadingLeave ? (
) : leaveRequests.length === 0 ? (
Không có đơn xin nghỉ phép nào cho ca học này trong ngày hôm nay.
) : ( leaveRequests.map((req) => (
{req.student.fullName}
{req.student.studentCode} {req.status}
{req.status === 'Đang chờ' && (
)}
Lý do nghỉ: {req.note || 'Không có ghi chú'}
{req.reasonImage && ( )}
)) )}
)} {selectedStudentRkIds.length > 0 && (
Đang chọn {selectedStudentRkIds.length} sinh viên:
{ATTENDANCE_STATUS_OPTIONS.map(opt => ( ))}
)}
{loading ? (
) : ( {filteredRows.length === 0 ? ( ) : ( filteredRows.map(row => { const isSelected = selectedStudentRkIds.includes(row.studentRkId); return ( ); }) )}
0 && filteredRows.every(r => selectedStudentRkIds.includes(r.studentRkId))} onChange={(e) => { if (e.target.checked) { const newIds = new Set(selectedStudentRkIds); filteredRows.forEach(r => newIds.add(r.studentRkId)); setSelectedStudentRkIds(Array.from(newIds)); } else { const filteredIds = filteredRows.map(r => r.studentRkId); setSelectedStudentRkIds(selectedStudentRkIds.filter(id => !filteredIds.includes(id))); } }} /> Sinh viên Online Trạng thái QLĐT / Ghi chú
{rows.length === 0 ? 'Chưa có dữ liệu điểm danh' : 'Không tìm thấy sinh viên phù hợp'}
toggleStudentSelection(row.studentRkId)} > {}} onClick={(e) => { e.stopPropagation(); toggleStudentSelection(row.studentRkId); }} /> toggleStudentSelection(row.studentRkId)} >
{row.fullName}
{row.studentCode} {row.email && {row.email}}
{row.onlineMinutes} phút
{row.pushedToQldtAt ? ( QLĐT ✓ ) : ( Chưa QLĐT )} {row.statusEditedByTeacher && ( Đã khóa )}
)}
); };