fix ui
All checks were successful
Deploy on Master Change / deploy (push) Successful in 47s

This commit is contained in:
2026-07-03 18:28:54 +07:00
parent 4d17699775
commit ea73c15738

View File

@@ -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 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 &quot;Đy QLĐT&quot; 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 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 &quot;Đy QLĐT&quot; 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ộ <strong>KHÔNG tự đng lưu lên QLĐT</strong>. Thầy 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ộ <strong>KHÔNG tự đng lưu lên QLĐT</strong>. Thầy 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>
) : ( ) : (