fix
All checks were successful
Deploy on Master Change / deploy (push) Successful in 53s

This commit is contained in:
2026-07-03 17:59:02 +07:00
parent 0fa4a9aae2
commit 96391b0706
13 changed files with 10448 additions and 100 deletions

View File

@@ -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;

View File

@@ -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> sinh viên đang đưc giám sát lớp khác lịch học
</p>
</div>
<button className="modal-close-btn" onClick={() => setConflictWarning(null)} aria-label="Đóng">&times;</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 lịch học hệ thống sẽ gắn họ vào lớp lịch, không phải lớp mới này.
Bạn vẫn 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>
);
};

View File

@@ -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%;
}