This commit is contained in:
@@ -42,6 +42,7 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const [leaveRequests, setLeaveRequests] = useState<LeaveRequestItem[]>([]);
|
const [leaveRequests, setLeaveRequests] = useState<LeaveRequestItem[]>([]);
|
||||||
const [loadingLeave, setLoadingLeave] = useState(false);
|
const [loadingLeave, setLoadingLeave] = useState(false);
|
||||||
|
const [showLeaveModal, setShowLeaveModal] = useState(false);
|
||||||
|
|
||||||
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const scrollTop = e.currentTarget.scrollTop;
|
const scrollTop = e.currentTarget.scrollTop;
|
||||||
@@ -273,8 +274,33 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
<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>
|
||||||
|
{courseId && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setShowLeaveModal(true)}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
|
||||||
|
>
|
||||||
|
✉️ Đơn phép
|
||||||
|
{leaveRequests.length > 0 && (
|
||||||
|
<span
|
||||||
|
className="badge"
|
||||||
|
style={{
|
||||||
|
background: leaveRequests.some(r => r.status === 'Đang chờ') ? 'var(--accent)' : 'var(--text-secondary)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
padding: '0.1rem 0.35rem',
|
||||||
|
borderRadius: '10px',
|
||||||
|
fontWeight: 700
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leaveRequests.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button type="button" className="btn btn-primary" onClick={handlePushQLDT} disabled={pushing || rows.length === 0}>
|
<button type="button" className="btn btn-primary" onClick={handlePushQLDT} disabled={pushing || rows.length === 0}>
|
||||||
{pushing ? 'Đang đẩy...' : 'Đẩy QLĐT'}
|
{pushing ? 'Đang tải...' : 'Đẩy QLĐT'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -342,130 +368,126 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Leave Requests Approval Section */}
|
{/* Leave Requests Approval Modal */}
|
||||||
{courseId && (
|
{showLeaveModal && courseId && (
|
||||||
<div
|
<div className="modal-overlay" onClick={() => setShowLeaveModal(false)}>
|
||||||
className="leave-requests-section"
|
<div className="modal-container" style={{ maxWidth: '680px', width: '95%', display: 'flex', flexDirection: 'column' }} onClick={e => e.stopPropagation()}>
|
||||||
style={{
|
<div className="modal-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
margin: '10px 0',
|
<div>
|
||||||
padding: '1rem',
|
<h3 className="modal-title" style={{ margin: 0 }}>✉️ Đơn xin nghỉ phép trong ca</h3>
|
||||||
background: 'rgba(255, 255, 255, 0.8)',
|
<p style={{ margin: '4px 0 0 0', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>
|
||||||
backdropFilter: 'blur(8px)',
|
Ca {period} ngày {date} - Giao diện duyệt đơn xin nghỉ từ cổng QLĐT portal.
|
||||||
border: '1px solid var(--border-color)',
|
</p>
|
||||||
borderRadius: '12px',
|
</div>
|
||||||
boxShadow: 'var(--shadow-sm)'
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
}}
|
<button
|
||||||
>
|
type="button"
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
className="btn btn-secondary btn-sm"
|
||||||
<h4 style={{ margin: 0, fontSize: '0.95rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--text-primary)' }}>
|
onClick={loadLeaveRequests}
|
||||||
<span>✉️ Đơn xin nghỉ phép trong ca</span>
|
disabled={loadingLeave}
|
||||||
<span className="badge" style={{ background: 'var(--accent-light)', color: 'var(--accent)', fontSize: '0.75rem', padding: '0.15rem 0.4rem', borderRadius: '10px' }}>
|
style={{ padding: '0.3rem 0.6rem', fontSize: '0.78rem' }}
|
||||||
{leaveRequests.length} đơn
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
onClick={loadLeaveRequests}
|
|
||||||
disabled={loadingLeave}
|
|
||||||
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem' }}
|
|
||||||
>
|
|
||||||
{loadingLeave ? 'Đang tải...' : '🔄 Làm mới đơn phép'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loadingLeave ? (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '1rem 0' }}>
|
|
||||||
<div className="sync-spinner" style={{ width: 20, height: 20 }} />
|
|
||||||
</div>
|
|
||||||
) : leaveRequests.length === 0 ? (
|
|
||||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.8rem', fontStyle: 'italic' }}>
|
|
||||||
Không có đơn xin nghỉ phép nào cho ca học này trong ngày hôm nay.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxHeight: '250px', overflowY: 'auto', paddingRight: '4px' }}>
|
|
||||||
{leaveRequests.map((req) => (
|
|
||||||
<div
|
|
||||||
key={req.id}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '6px',
|
|
||||||
padding: '0.75rem',
|
|
||||||
border: '1px solid var(--border-color)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
backgroundColor: '#ffffff'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '8px' }}>
|
{loadingLeave ? 'Đang tải...' : '🔄 Làm mới'}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
</button>
|
||||||
<div style={{ fontWeight: 600, fontSize: '0.85rem' }}>{req.student.fullName}</div>
|
<button type="button" className="btn btn-secondary btn-sm" onClick={() => setShowLeaveModal(false)}>
|
||||||
<code style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{req.student.studentCode}</code>
|
Đóng
|
||||||
<span
|
</button>
|
||||||
className="badge"
|
</div>
|
||||||
style={{
|
</div>
|
||||||
fontSize: '0.7rem',
|
<div className="modal-body" style={{ padding: '1rem', overflowY: 'auto', maxHeight: '60vh', display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
fontWeight: 600,
|
{loadingLeave ? (
|
||||||
backgroundColor: req.status === 'Đang chờ' ? '#fef3c7' : req.status === 'Phê duyệt' ? '#d1fae5' : '#fee2e2',
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem 0' }}>
|
||||||
color: req.status === 'Đang chờ' ? '#b45309' : req.status === 'Phê duyệt' ? '#065f46' : '#991b1b',
|
<div className="sync-spinner" style={{ width: 28, height: 28 }} />
|
||||||
padding: '0.1rem 0.4rem',
|
</div>
|
||||||
borderRadius: '4px'
|
) : leaveRequests.length === 0 ? (
|
||||||
}}
|
<div style={{ textAlign: 'center', padding: '2rem 0', color: 'var(--text-muted)', fontSize: '0.85rem', fontStyle: 'italic' }}>
|
||||||
>
|
Không có đơn xin nghỉ phép nào cho ca học này trong ngày hôm nay.
|
||||||
{req.status}
|
</div>
|
||||||
</span>
|
) : (
|
||||||
|
leaveRequests.map((req) => (
|
||||||
|
<div
|
||||||
|
key={req.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '0.85rem',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
boxShadow: 'var(--shadow-sm)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '0.9rem' }}>{req.student.fullName}</div>
|
||||||
|
<code style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>{req.student.studentCode}</code>
|
||||||
|
<span
|
||||||
|
className="badge"
|
||||||
|
style={{
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: req.status === 'Đang chờ' ? '#fef3c7' : req.status === 'Phê duyệt' ? '#d1fae5' : '#fee2e2',
|
||||||
|
color: req.status === 'Đang chờ' ? '#b45309' : req.status === 'Phê duyệt' ? '#065f46' : '#991b1b',
|
||||||
|
padding: '0.15rem 0.45rem',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{req.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{req.status === 'Đang chờ' && (
|
||||||
|
<div style={{ display: 'flex', gap: '6px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
style={{ padding: '0.25rem 0.65rem', fontSize: '0.78rem', backgroundColor: 'var(--success)', borderColor: 'var(--success)' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Phê duyệt đơn xin nghỉ phép của ${req.student.fullName}? Trạng thái điểm danh ca sẽ chuyển thành Nghỉ có phép.`)) {
|
||||||
|
handleUpdateLeaveStatus(req.id, 'Phê duyệt', req.student.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓ Phê duyệt
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm text-danger"
|
||||||
|
style={{ padding: '0.25rem 0.65rem', fontSize: '0.78rem', borderColor: '#fca5a5' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Từ chối đơn xin nghỉ phép của ${req.student.fullName}?`)) {
|
||||||
|
handleUpdateLeaveStatus(req.id, 'Từ chối', req.student.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✗ Từ chối
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{req.status === 'Đang chờ' && (
|
<div style={{ fontSize: '0.82rem', color: 'var(--text-primary)', background: '#f9fafb', padding: '8px 10px', borderRadius: '4px', borderLeft: '4px solid #cbd5e1', lineHeight: '1.4' }}>
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<strong>Lý do nghỉ:</strong> {req.note || 'Không có ghi chú'}
|
||||||
<button
|
</div>
|
||||||
type="button"
|
|
||||||
className="btn btn-primary btn-sm"
|
{req.reasonImage && (
|
||||||
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem', backgroundColor: 'var(--success)', borderColor: 'var(--success)' }}
|
<div style={{ marginTop: '4px' }}>
|
||||||
onClick={() => {
|
<a
|
||||||
if (confirm(`Phê duyệt đơn xin nghỉ phép của ${req.student.fullName}? Trạng thái điểm danh ca sẽ chuyển thành Nghỉ có phép.`)) {
|
href={req.reasonImage}
|
||||||
handleUpdateLeaveStatus(req.id, 'Phê duyệt', req.student.id);
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
}}
|
style={{ fontSize: '0.75rem', color: 'var(--accent)', textDecoration: 'underline', display: 'inline-flex', alignItems: 'center', gap: '4px', fontWeight: 500 }}
|
||||||
>
|
>
|
||||||
✓ Phê duyệt
|
🖼️ Xem ảnh minh chứng phép
|
||||||
</button>
|
</a>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm text-danger"
|
|
||||||
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem', borderColor: '#fca5a5' }}
|
|
||||||
onClick={() => {
|
|
||||||
if (confirm(`Từ chối đơn xin nghỉ phép của ${req.student.fullName}?`)) {
|
|
||||||
handleUpdateLeaveStatus(req.id, 'Từ chối', req.student.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✗ Từ chối
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-primary)', background: '#f9fafb', padding: '6px 8px', borderRadius: '4px', borderLeft: '3px solid #cbd5e1' }}>
|
)}
|
||||||
<strong>Lý do nghỉ:</strong> {req.note || 'Không có ghi chú'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{req.reasonImage && (
|
|
||||||
<div style={{ marginTop: '4px' }}>
|
|
||||||
<a
|
|
||||||
href={req.reasonImage}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{ fontSize: '0.72rem', color: 'var(--accent)', textDecoration: 'underline', display: 'flex', alignItems: 'center', gap: '3px' }}
|
|
||||||
>
|
|
||||||
🖼️ Xem ảnh minh chứng phép
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user