This commit is contained in:
@@ -722,6 +722,21 @@ export const apiFetchOnlineStudents = async (rkId: number): Promise<{ onlineStud
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export interface ScheduleConflictItem {
|
||||
studentRkId: number;
|
||||
fullName: string;
|
||||
studentCode: string;
|
||||
conflictClassRkId: number;
|
||||
conflictClassName: string;
|
||||
conflictClassCode: string;
|
||||
}
|
||||
|
||||
export const apiFetchScheduleConflicts = async (rkId: number): Promise<{ conflicts: ScheduleConflictItem[]; ok: boolean }> => {
|
||||
const res = await staffFetch(`/classes/${rkId}/schedule-conflicts`);
|
||||
if (!res.ok) throw new Error('Failed to check schedule conflicts');
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export const apiFetchClassCourses = async (rkId: number): Promise<{
|
||||
data: ClassCourseItem[];
|
||||
source?: string;
|
||||
|
||||
@@ -6,13 +6,15 @@ import {
|
||||
apiFetchAllowedApps,
|
||||
apiSaveAllowedApps,
|
||||
apiFetchClassSessionLogs,
|
||||
apiFetchClasses
|
||||
apiFetchClasses,
|
||||
apiFetchScheduleConflicts,
|
||||
} from '../api';
|
||||
import { DEFAULT_ALLOWED_APPS } from '../constants';
|
||||
import type {
|
||||
ClassItem,
|
||||
ClassScheduleItem,
|
||||
StudentSessionLogItem
|
||||
StudentSessionLogItem,
|
||||
ScheduleConflictItem,
|
||||
} from '../api';
|
||||
import { LiveProctorModal } from './LiveProctorModal';
|
||||
import { openClass } from './NavHistoryBar';
|
||||
@@ -49,6 +51,7 @@ export const LearningTab: React.FC = () => {
|
||||
const [searchClassQuery, setSearchClassQuery] = useState<string>('');
|
||||
|
||||
const [proctorStudent, setProctorStudent] = useState<{ id: number; name: string; code: string } | null>(null);
|
||||
const [conflictWarning, setConflictWarning] = useState<{ cls: ClassItem; conflicts: ScheduleConflictItem[] } | null>(null);
|
||||
|
||||
const loadActiveClasses = async () => {
|
||||
setLoading(true);
|
||||
@@ -89,6 +92,20 @@ export const LearningTab: React.FC = () => {
|
||||
|
||||
const handleActivateClass = async (cls: ClassItem) => {
|
||||
setShowAddClassModal(false);
|
||||
try {
|
||||
// Kiểm tra trùng sinh viên với lớp đang giám sát có lịch học
|
||||
const conflictRes = await apiFetchScheduleConflicts(cls.rkId);
|
||||
if (conflictRes.conflicts && conflictRes.conflicts.length > 0) {
|
||||
setConflictWarning({ cls, conflicts: conflictRes.conflicts });
|
||||
return;
|
||||
}
|
||||
await doActivateClass(cls);
|
||||
} catch (err) {
|
||||
alert('Không thể kích hoạt lớp: ' + err);
|
||||
}
|
||||
};
|
||||
|
||||
const doActivateClass = async (cls: ClassItem) => {
|
||||
try {
|
||||
await apiSaveAllowedApps(cls.rkId, DEFAULT_ALLOWED_APPS);
|
||||
await loadActiveClasses();
|
||||
@@ -604,6 +621,59 @@ export const LearningTab: React.FC = () => {
|
||||
onClose={() => setProctorStudent(null)}
|
||||
/>
|
||||
)}
|
||||
{conflictWarning && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-container" style={{ maxWidth: '560px' }}>
|
||||
<div className="modal-header">
|
||||
<div>
|
||||
<h2 className="modal-title" style={{ color: 'var(--danger)' }}>⚠️ Cảnh báo trùng sinh viên</h2>
|
||||
<p style={{ margin: '4px 0 0 0', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
|
||||
Lớp <strong>{conflictWarning.cls.classCode}</strong> có sinh viên đang được giám sát ở lớp khác có lịch học
|
||||
</p>
|
||||
</div>
|
||||
<button className="modal-close-btn" onClick={() => setConflictWarning(null)} aria-label="Đóng">×</button>
|
||||
</div>
|
||||
<div className="modal-body" style={{ padding: '1.25rem 1.5rem', maxHeight: '340px', overflowY: 'auto' }}>
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: 0, marginBottom: '0.85rem' }}>
|
||||
Những sinh viên bên dưới đang thuộc lớp đang giám sát có lịch học — hệ thống sẽ gắn họ vào lớp có lịch, không phải lớp mới này.
|
||||
Bạn vẫn có thể tiếp tục kích hoạt, nhưng nên kiểm tra lại danh sách lớp để tránh nhầm lẫn.
|
||||
</p>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.82rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--border-color)', textAlign: 'left' }}>
|
||||
<th style={{ padding: '0.4rem 0.5rem', color: 'var(--text-muted)', fontWeight: 600 }}>Sinh viên</th>
|
||||
<th style={{ padding: '0.4rem 0.5rem', color: 'var(--text-muted)', fontWeight: 600 }}>Đang ở lớp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conflictWarning.conflicts.map((cf, idx) => (
|
||||
<tr key={idx} style={{ borderBottom: '1px solid var(--border-light)' }}>
|
||||
<td style={{ padding: '0.45rem 0.5rem' }}>
|
||||
<div style={{ fontWeight: 600 }}>{cf.fullName}</div>
|
||||
<div style={{ color: 'var(--text-muted)', fontFamily: 'monospace' }}>{cf.studentCode}</div>
|
||||
</td>
|
||||
<td style={{ padding: '0.45rem 0.5rem' }}>
|
||||
<div style={{ fontWeight: 600, color: 'var(--danger)' }}>{cf.conflictClassCode}</div>
|
||||
<div style={{ color: 'var(--text-secondary)', fontSize: '0.78rem' }}>{cf.conflictClassName}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="modal-footer" style={{ gap: '0.65rem' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setConflictWarning(null)}>Hủy</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}
|
||||
onClick={() => { const cls = conflictWarning.cls; setConflictWarning(null); doActivateClass(cls); }}
|
||||
>
|
||||
Vẫn kích hoạt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1972,6 +1972,8 @@ input:checked + .slider:before {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.attendance-table-scroll {
|
||||
@@ -1988,6 +1990,10 @@ input:checked + .slider:before {
|
||||
}
|
||||
|
||||
.attendance-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user