fix add more student
This commit is contained in:
@@ -69,6 +69,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
|
const [submissions, setSubmissions] = useState<ExamSubmission[]>([]);
|
||||||
const [searchQ, setSearchQ] = useState('');
|
const [searchQ, setSearchQ] = useState('');
|
||||||
const [searchHits, setSearchHits] = useState<{ studentRkId: number; fullName: string; studentCode: string; email: string; classCodes: string }[]>([]);
|
const [searchHits, setSearchHits] = useState<{ studentRkId: number; fullName: string; studentCode: string; email: string; classCodes: string }[]>([]);
|
||||||
|
const [selectedPickerRkIds, setSelectedPickerRkIds] = useState<number[]>([]);
|
||||||
const [paperTitle, setPaperTitle] = useState('');
|
const [paperTitle, setPaperTitle] = useState('');
|
||||||
const [packagePdf, setPackagePdf] = useState<File | null>(null);
|
const [packagePdf, setPackagePdf] = useState<File | null>(null);
|
||||||
const [packageResources, setPackageResources] = useState<File[]>([]);
|
const [packageResources, setPackageResources] = useState<File[]>([]);
|
||||||
@@ -210,6 +211,39 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
await fetchOnline();
|
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 () => {
|
const createPackage = async () => {
|
||||||
if (!packagePdf) {
|
if (!packagePdf) {
|
||||||
setErr('Chọn file PDF cho gói đề');
|
setErr('Chọn file PDF cho gói đề');
|
||||||
@@ -820,7 +854,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); }}
|
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); setSelectedPickerRkIds([]); }}
|
||||||
>
|
>
|
||||||
+ Thêm sinh viên
|
+ Thêm sinh viên
|
||||||
</button>
|
</button>
|
||||||
@@ -846,7 +880,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); }}
|
onClick={() => { setStudentPickerOpen(true); setSearchQ(''); setSearchHits([]); setSelectedPickerRkIds([]); }}
|
||||||
>
|
>
|
||||||
+ Thêm sinh viên
|
+ Thêm sinh viên
|
||||||
</button>
|
</button>
|
||||||
@@ -1168,15 +1202,31 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="exam-student-modal-body">
|
<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
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="search-input"
|
className="search-input"
|
||||||
placeholder="Tìm sinh viên..."
|
placeholder="Tìm sinh viên..."
|
||||||
value={searchQ}
|
value={searchQ}
|
||||||
onChange={(e) => setSearchQ(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchQ(e.target.value);
|
||||||
|
setSelectedPickerRkIds([]);
|
||||||
|
}}
|
||||||
autoFocus
|
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>
|
||||||
<div className="exam-student-hits">
|
<div className="exam-student-hits">
|
||||||
{searchQ.trim() === '' ? (
|
{searchQ.trim() === '' ? (
|
||||||
@@ -1192,6 +1242,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
) : (
|
) : (
|
||||||
searchHits.map((h) => {
|
searchHits.map((h) => {
|
||||||
const added = enrolledIds.has(h.studentRkId);
|
const added = enrolledIds.has(h.studentRkId);
|
||||||
|
const isSelected = selectedPickerRkIds.includes(h.studentRkId);
|
||||||
const initials = h.fullName
|
const initials = h.fullName
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -1199,7 +1250,27 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
.map((w) => w[0]?.toUpperCase() || '')
|
.map((w) => w[0]?.toUpperCase() || '')
|
||||||
.join('');
|
.join('');
|
||||||
return (
|
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-avatar" aria-hidden="true">{initials || '?'}</div>
|
||||||
<div className="exam-student-hit-info">
|
<div className="exam-student-hit-info">
|
||||||
<strong>{h.fullName}</strong>
|
<strong>{h.fullName}</strong>
|
||||||
@@ -1213,7 +1284,10 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm ${added ? 'btn-secondary' : 'btn-primary'}`}
|
className={`btn btn-sm ${added ? 'btn-secondary' : 'btn-primary'}`}
|
||||||
disabled={added}
|
disabled={added}
|
||||||
onClick={() => addStudent(h.studentRkId)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addStudent(h.studentRkId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{added ? '✓ Đã thêm' : '+ Thêm'}
|
{added ? '✓ Đã thêm' : '+ Thêm'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1223,6 +1297,31 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4367,7 +4367,7 @@ input:checked + .slider:before {
|
|||||||
|
|
||||||
.exam-student-hit {
|
.exam-student-hit {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto auto 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
@@ -4377,6 +4377,15 @@ input:checked + .slider:before {
|
|||||||
transition: var(--transition);
|
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 {
|
.exam-student-hit:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 2px 10px rgba(187, 33, 38, 0.08);
|
box-shadow: 0 2px 10px rgba(187, 33, 38, 0.08);
|
||||||
|
|||||||
Reference in New Issue
Block a user