This commit is contained in:
2026-06-30 13:08:18 +07:00
parent 795726aea5
commit 1c2bfde005
18 changed files with 1504 additions and 90 deletions

View File

@@ -140,7 +140,7 @@ function App() {
onClick={() => navigate('learning')}
>
<span className="nav-icon"><IconLearning /></span>
Giám sát & Lịch học
Phòng Học
</button>
</li>
<li className="nav-item">

View File

@@ -816,53 +816,70 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
)}
{studentPickerOpen && (
<div className="modal-overlay">
<div className="modal-container" style={{ maxWidth: '560px', width: '92%' }}>
<div className="modal-header">
<div className="modal-overlay exam-student-overlay" onClick={() => setStudentPickerOpen(false)}>
<div className="modal-container exam-student-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header exam-student-modal-header">
<div>
<h2 className="modal-title">Thêm sinh viên</h2>
<p style={{ margin: '4px 0 0', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
Tìm theo lớp, họ tên, sinh viên
</p>
<p className="exam-student-modal-desc">Tìm theo lớp, họ tên, sinh viên</p>
</div>
<button type="button" className="btn btn-secondary close-btn" onClick={() => setStudentPickerOpen(false)}>Đóng</button>
<button type="button" className="modal-close-btn" onClick={() => setStudentPickerOpen(false)} aria-label="Đóng">
×
</button>
</div>
<div style={{ padding: '0 0 1rem' }}>
<input
type="search"
className="app-pool-search"
placeholder="Tìm sinh viên..."
value={searchQ}
onChange={(e) => setSearchQ(e.target.value)}
autoFocus
/>
<div style={{ maxHeight: 'min(55vh, 420px)', overflowY: 'auto', marginTop: '1rem' }}>
<div className="exam-student-modal-body">
<div className="exam-student-modal-search search-input-wrap">
<input
type="search"
className="search-input"
placeholder="Tìm sinh viên..."
value={searchQ}
onChange={(e) => setSearchQ(e.target.value)}
autoFocus
/>
</div>
<div className="exam-student-hits">
{searchQ.trim() === '' ? (
<p style={{ color: 'var(--text-muted)', textAlign: 'center' }}> ít nhất vài tự đ tìm.</p>
<div className="exam-student-hits-empty">
<span>🔍</span>
<p> ít nhất vài tự đ tìm sinh viên.</p>
</div>
) : searchHits.length === 0 ? (
<p style={{ color: 'var(--text-muted)', textAlign: 'center' }}>Không tìm thấy sinh viên phù hợp.</p>
<div className="exam-student-hits-empty">
<span>📭</span>
<p>Không tìm thấy sinh viên phù hợp.</p>
</div>
) : (
<ul className="learning-add-list">
{searchHits.map((h) => {
const added = enrolledIds.has(h.studentRkId);
return (
<li key={h.studentRkId} className="learning-add-item">
<div>
<div className="learning-class-code">{h.studentCode}</div>
<div className="learning-class-meta">{h.fullName}</div>
{(h.classCodes || h.email) && (
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.2rem' }}>
{[h.classCodes, h.email].filter(Boolean).join(' · ')}
</div>
)}
searchHits.map((h) => {
const added = enrolledIds.has(h.studentRkId);
const initials = h.fullName
.split(/\s+/)
.filter(Boolean)
.slice(-2)
.map((w) => w[0]?.toUpperCase() || '')
.join('');
return (
<div key={h.studentRkId} className="exam-student-hit">
<div className="exam-student-hit-avatar" aria-hidden="true">{initials || '?'}</div>
<div className="exam-student-hit-info">
<strong>{h.fullName}</strong>
<div className="exam-student-hit-meta">
SV: <code>{h.studentCode}</code>
{h.classCodes ? <> · Lớp: {h.classCodes}</> : null}
{h.email ? <> · {h.email}</> : null}
</div>
<button type="button" className="btn btn-primary btn-sm" disabled={added} onClick={() => addStudent(h.studentRkId)}>
{added ? 'Đã thêm' : 'Thêm'}
</button>
</li>
);
})}
</ul>
</div>
<button
type="button"
className={`btn btn-sm ${added ? 'btn-secondary' : 'btn-primary'}`}
disabled={added}
onClick={() => addStudent(h.studentRkId)}
>
{added ? '✓ Đã thêm' : '+ Thêm'}
</button>
</div>
);
})
)}
</div>
</div>

View File

@@ -4212,6 +4212,21 @@ input:checked + .slider:before {
box-shadow: 0 2px 10px rgba(187, 33, 38, 0.08);
}
.exam-student-hit-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #fff5f5 0%, #fee2e2 100%);
color: var(--accent);
font-weight: 700;
font-size: 0.82rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid rgba(187, 33, 38, 0.12);
}
.exam-student-hit-info {
min-width: 0;
}