fix add more student

This commit is contained in:
2026-06-30 16:15:30 +07:00
parent 3e53cb6664
commit 1d54acccbc
2 changed files with 115 additions and 7 deletions

View File

@@ -69,6 +69,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
const [searchQ, setSearchQ] = useState('');
const [searchHits, setSearchHits] = useState<{ studentRkId: number; fullName: string; studentCode: string; email: string; classCodes: string }[]>([]);
const [selectedPickerRkIds, setSelectedPickerRkIds] = useState<number[]>([]);
const [paperTitle, setPaperTitle] = useState('');
const [packagePdf, setPackagePdf] = useState<File | null>(null);
const [packageResources, setPackageResources] = useState<File[]>([]);
@@ -210,6 +211,39 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
await fetchOnline();
};
const toggleHitSelection = (studentRkId: number) => {
setSelectedPickerRkIds((prev) =>
prev.includes(studentRkId) ? prev.filter((id) => id !== studentRkId) : [...prev, studentRkId]
);
};
const addSelectedStudents = async () => {
if (selectedPickerRkIds.length === 0) return;
try {
await apiExam.addStudents(examId, selectedPickerRkIds);
setSelectedPickerRkIds([]);
setStudentPickerOpen(false);
await load();
await fetchOnline();
} catch (e: any) {
alert(e?.message || 'Thêm sinh viên thất bại');
}
};
const toggleSelectAllHits = () => {
const enrolledIds = new Set(students.map((s) => s.studentRkId));
const unaddedHits = searchHits.filter((h) => !enrolledIds.has(h.studentRkId));
const allUnaddedIds = unaddedHits.map((h) => h.studentRkId);
if (allUnaddedIds.length === 0) return;
const areAllSelected = allUnaddedIds.every((id) => selectedPickerRkIds.includes(id));
if (areAllSelected) {
setSelectedPickerRkIds((prev) => prev.filter((id) => !allUnaddedIds.includes(id)));
} else {
setSelectedPickerRkIds((prev) => Array.from(new Set([...prev, ...allUnaddedIds])));
}
};
const createPackage = async () => {
if (!packagePdf) {
setErr('Chọn file PDF cho gói đề');
@@ -820,7 +854,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); }}
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); setSelectedPickerRkIds([]); }}
>
+ Thêm sinh viên
</button>
@@ -846,7 +880,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
<button
type="button"
className="btn btn-primary"
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); }}
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); setSelectedPickerRkIds([]); }}
>
+ Thêm sinh viên
</button>
@@ -1168,15 +1202,31 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
</button>
</div>
<div className="exam-student-modal-body">
<div className="exam-student-modal-search search-input-wrap">
<div className="exam-student-modal-search search-input-wrap" style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<input
type="search"
className="search-input"
placeholder="Tìm sinh viên..."
value={searchQ}
onChange={(e) => setSearchQ(e.target.value)}
onChange={(e) => {
setSearchQ(e.target.value);
setSelectedPickerRkIds([]);
}}
autoFocus
style={{ flex: 1 }}
/>
{searchQ.trim() !== '' && searchHits.some((h) => !enrolledIds.has(h.studentRkId)) && (
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={toggleSelectAllHits}
style={{ whiteSpace: 'nowrap' }}
>
{searchHits.filter((h) => !enrolledIds.has(h.studentRkId)).every((h) => selectedPickerRkIds.includes(h.studentRkId))
? 'Bỏ chọn tất cả'
: 'Chọn tất cả'}
</button>
)}
</div>
<div className="exam-student-hits">
{searchQ.trim() === '' ? (
@@ -1192,6 +1242,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
) : (
searchHits.map((h) => {
const added = enrolledIds.has(h.studentRkId);
const isSelected = selectedPickerRkIds.includes(h.studentRkId);
const initials = h.fullName
.split(/\s+/)
.filter(Boolean)
@@ -1199,7 +1250,27 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
.map((w) => w[0]?.toUpperCase() || '')
.join('');
return (
<div key={h.studentRkId} className="exam-student-hit">
<div
key={h.studentRkId}
className={`exam-student-hit ${added ? 'exam-student-hit--added' : ''} ${isSelected ? 'exam-student-hit--selected' : ''}`}
style={{ cursor: added ? 'default' : 'pointer' }}
onClick={() => {
if (!added) toggleHitSelection(h.studentRkId);
}}
>
<div
style={{ display: 'flex', alignItems: 'center' }}
onClick={(e) => e.stopPropagation()}
>
<input
type="checkbox"
checked={added || isSelected}
disabled={added}
onChange={() => {
if (!added) toggleHitSelection(h.studentRkId);
}}
/>
</div>
<div className="exam-student-hit-avatar" aria-hidden="true">{initials || '?'}</div>
<div className="exam-student-hit-info">
<strong>{h.fullName}</strong>
@@ -1213,7 +1284,10 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
type="button"
className={`btn btn-sm ${added ? 'btn-secondary' : 'btn-primary'}`}
disabled={added}
onClick={() => addStudent(h.studentRkId)}
onClick={(e) => {
e.stopPropagation();
addStudent(h.studentRkId);
}}
>
{added ? '✓ Đã thêm' : '+ Thêm'}
</button>
@@ -1223,6 +1297,31 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
)}
</div>
</div>
<div className="modal-footer exam-student-modal-footer" style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '0.75rem',
padding: '0.85rem 1.25rem',
borderTop: '1px solid var(--border-color, #dee2e6)',
background: 'var(--bg-subtle, #f8f9fa)',
borderRadius: '0 0 var(--radius-lg) var(--radius-lg)'
}}>
<button
type="button"
className="btn btn-secondary"
onClick={() => setStudentPickerOpen(false)}
>
Đóng
</button>
<button
type="button"
className="btn btn-primary"
disabled={selectedPickerRkIds.length === 0}
onClick={addSelectedStudents}
>
Thêm {selectedPickerRkIds.length > 0 ? `(${selectedPickerRkIds.length})` : ''} sinh viên đã chọn
</button>
</div>
</div>
</div>
)}

View File

@@ -4367,7 +4367,7 @@ input:checked + .slider:before {
.exam-student-hit {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 0.85rem;
padding: 0.85rem 1rem;
@@ -4377,6 +4377,15 @@ input:checked + .slider:before {
transition: var(--transition);
}
.exam-student-hit--added {
opacity: 0.85;
}
.exam-student-hit--selected {
border-color: var(--accent);
background-color: var(--accent-light, #fff5f5);
}
.exam-student-hit:hover {
border-color: var(--accent);
box-shadow: 0 2px 10px rgba(187, 33, 38, 0.08);