This commit is contained in:
@@ -36,6 +36,16 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
const [pushing, setPushing] = useState(false);
|
const [pushing, setPushing] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedStudentRkIds, setSelectedStudentRkIds] = useState<number[]>([]);
|
const [selectedStudentRkIds, setSelectedStudentRkIds] = useState<number[]>([]);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const scrollTop = e.currentTarget.scrollTop;
|
||||||
|
if (scrollTop > 20) {
|
||||||
|
setIsCollapsed(true);
|
||||||
|
} else if (scrollTop <= 5) {
|
||||||
|
setIsCollapsed(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadShifts = useCallback(async () => {
|
const loadShifts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -210,6 +220,14 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
title={isCollapsed ? 'Hiện đầy đủ thông tin chi tiết' : 'Ẩn bớt thông tin chi tiết để xem bảng to hơn'}
|
||||||
|
>
|
||||||
|
{isCollapsed ? '📂 Chi tiết' : '📁 Thu gọn'}
|
||||||
|
</button>
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => loadAttendance(false)} disabled={loading}>
|
<button type="button" className="btn btn-secondary" onClick={() => loadAttendance(false)} disabled={loading}>
|
||||||
Tải lại
|
Tải lại
|
||||||
</button>
|
</button>
|
||||||
@@ -218,67 +236,71 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{!isCollapsed && (
|
||||||
className={`attendance-qldt-banner${
|
<>
|
||||||
qldtSynced ? (qldtDirty ? ' attendance-qldt-banner--stale' : ' attendance-qldt-banner--synced') : ''
|
<div
|
||||||
}`}
|
className={`attendance-qldt-banner${
|
||||||
>
|
qldtSynced ? (qldtDirty ? ' attendance-qldt-banner--stale' : ' attendance-qldt-banner--synced') : ''
|
||||||
{qldtSynced ? (
|
}`}
|
||||||
qldtDirty ? (
|
|
||||||
<>
|
|
||||||
<strong>Đã lưu QLĐT — có thay đổi mới</strong>
|
|
||||||
<span>
|
|
||||||
Lần đẩy: {formatQldtTime(shiftInfo?.pushedToQldtAt)} · Cần đẩy lại sau khi chỉnh sửa
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<strong>Đã lưu QLĐT</strong>
|
|
||||||
<span>Lần đẩy gần nhất: {formatQldtTime(shiftInfo?.pushedToQldtAt)}</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<strong>Chưa đẩy lên QLĐT</strong>
|
|
||||||
<span>Ca {period} ngày {date} — bấm "Đẩy QLĐT" sau khi kiểm tra trạng thái</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentShift && (
|
|
||||||
<div className="attendance-shift-info">
|
|
||||||
<span>{currentShift.startTime}–{currentShift.endTime}</span>
|
|
||||||
<span>{currentShift.courseName || 'Chưa chọn môn'}</span>
|
|
||||||
{!currentShift.isActive && <span className="badge badge-muted">Ca tắt</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="attendance-status-summary">
|
|
||||||
{ATTENDANCE_STATUS_OPTIONS.map(opt => (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
type="button"
|
|
||||||
className={`attendance-summary-chip ${attendanceStatusClass(opt.value)}`}
|
|
||||||
onClick={() => setSearchQuery('')}
|
|
||||||
title={`${opt.label}: ${statusCounts[opt.value] ?? 0} sinh viên`}
|
|
||||||
>
|
>
|
||||||
<span className="attendance-summary-chip-label">{opt.short}</span>
|
{qldtSynced ? (
|
||||||
<span className="attendance-summary-chip-count">{statusCounts[opt.value] ?? 0}</span>
|
qldtDirty ? (
|
||||||
</button>
|
<>
|
||||||
))}
|
<strong>Đã lưu QLĐT — có thay đổi mới</strong>
|
||||||
<span className="attendance-summary-total">
|
<span>
|
||||||
{filteredRows.length}/{rows.length} SV
|
Lần đẩy: {formatQldtTime(shiftInfo?.pushedToQldtAt)} · Cần đẩy lại sau khi chỉnh sửa
|
||||||
{searchQuery.trim() ? ' (đã lọc)' : ''}
|
</span>
|
||||||
</span>
|
</>
|
||||||
</div>
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>Đã lưu QLĐT</strong>
|
||||||
|
<span>Lần đẩy gần nhất: {formatQldtTime(shiftInfo?.pushedToQldtAt)}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>Chưa đẩy lên QLĐT</strong>
|
||||||
|
<span>Ca {period} ngày {date} — bấm "Đẩy QLĐT" sau khi kiểm tra trạng thái</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="schedule-hint" style={{ margin: '0 0 8px 0' }}>
|
{currentShift && (
|
||||||
Sửa trạng thái thủ công sẽ được khóa — hệ thống tự tính sẽ không ghi đè.
|
<div className="attendance-shift-info">
|
||||||
</p>
|
<span>{currentShift.startTime}–{currentShift.endTime}</span>
|
||||||
|
<span>{currentShift.courseName || 'Chưa chọn môn'}</span>
|
||||||
|
{!currentShift.isActive && <span className="badge badge-muted">Ca tắt</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ padding: '0.65rem 0.85rem', background: '#fef3c7', border: '1px solid #fcd34d', borderRadius: '4px', color: '#92400e', fontSize: '0.82rem', display: 'flex', alignItems: 'center', gap: '0.35rem', margin: '4px 0 10px 0', lineHeight: '1.4' }}>
|
<div className="attendance-status-summary">
|
||||||
💡 <strong>Lưu ý:</strong> Hệ thống hiện tại chỉ ghi nhận điểm danh học tập nội bộ và <strong>KHÔNG tự động lưu lên QLĐT</strong>. Thầy cô vui lòng kiểm tra kỹ trạng thái của sinh viên, sau đó bấm nút <strong>Đẩy QLĐT</strong> ở góc phải bên trên để đồng bộ điểm danh chính thức.
|
{ATTENDANCE_STATUS_OPTIONS.map(opt => (
|
||||||
</div>
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
className={`attendance-summary-chip ${attendanceStatusClass(opt.value)}`}
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
title={`${opt.label}: ${statusCounts[opt.value] ?? 0} sinh viên`}
|
||||||
|
>
|
||||||
|
<span className="attendance-summary-chip-label">{opt.short}</span>
|
||||||
|
<span className="attendance-summary-chip-count">{statusCounts[opt.value] ?? 0}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<span className="attendance-summary-total">
|
||||||
|
{filteredRows.length}/{rows.length} SV
|
||||||
|
{searchQuery.trim() ? ' (đã lọc)' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="schedule-hint" style={{ margin: '0 0 8px 0' }}>
|
||||||
|
Sửa trạng thái thủ công sẽ được khóa — hệ thống tự tính sẽ không ghi đè.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ padding: '0.65rem 0.85rem', background: '#fef3c7', border: '1px solid #fcd34d', borderRadius: '4px', color: '#92400e', fontSize: '0.82rem', display: 'flex', alignItems: 'center', gap: '0.35rem', margin: '4px 0 10px 0', lineHeight: '1.4' }}>
|
||||||
|
💡 <strong>Lưu ý:</strong> Hệ thống hiện tại chỉ ghi nhận điểm danh học tập nội bộ và <strong>KHÔNG tự động lưu lên QLĐT</strong>. Thầy cô vui lòng kiểm tra kỹ trạng thái của sinh viên, sau đó bấm nút <strong>Đẩy QLĐT</strong> ở góc phải bên trên để đồng bộ điểm danh chính thức.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedStudentRkIds.length > 0 && (
|
{selectedStudentRkIds.length > 0 && (
|
||||||
<div className="attendance-bulk-actions">
|
<div className="attendance-bulk-actions">
|
||||||
@@ -309,7 +331,7 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="attendance-table-scroll table-wrapper">
|
<div className="attendance-table-scroll table-wrapper" onScroll={handleScroll}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="empty-state"><div className="sync-spinner" style={{ width: 28, height: 28 }} /></div>
|
<div className="empty-state"><div className="sync-spinner" style={{ width: 28, height: 28 }} /></div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user