Files
rikkei_simple_care/management/src/components/LearningTab.tsx
PhuocNTB 06f2edb195
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m17s
fix random
2026-07-13 10:29:34 +07:00

763 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState } from 'react';
import {
apiFetchActiveSchedules,
apiFetchClassSchedule,
apiDeleteClassSchedule,
apiFetchAllowedApps,
apiSaveAllowedApps,
apiFetchClassSessionLogs,
apiFetchClasses,
apiFetchScheduleConflicts,
apiMyClasses,
} from '../api';
import { DEFAULT_ALLOWED_APPS } from '../constants';
import type {
ClassItem,
ClassScheduleItem,
StudentSessionLogItem,
ScheduleConflictItem,
} from '../api';
import { LiveProctorModal } from './LiveProctorModal';
import { AppPoolModal } from './AppPoolModal';
import { AppTemplatePickerModal } from './AppTemplatePickerModal';
import { mergeKeywordCSV } from '../utils/appKeywords';
import { openClass } from './NavHistoryBar';
import { ScheduleEditor } from './ScheduleEditor';
import { LearningTimetable } from './LearningTimetable';
import {
WEEK_DAYS,
filterActiveClasses,
countWeeklyShifts,
classHasToday,
type ActiveClassItem,
} from './learningUtils';
type ViewMode = 'list' | 'timetable';
export const LearningTab: React.FC = () => {
const [activeClasses, setActiveClasses] = useState<ActiveClassItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState('');
const [dayFilter, setDayFilter] = useState<number | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [onlyMyClasses, setOnlyMyClasses] = useState<boolean>(false);
const [myClasses, setMyClasses] = useState<number[]>([]);
const refreshMyClasses = () => {
apiMyClasses.list().then((items) => {
setMyClasses(items.map((c) => c.rkId));
}).catch(() => {});
};
useEffect(() => {
refreshMyClasses();
window.addEventListener('my-classes-updated', refreshMyClasses);
return () => window.removeEventListener('my-classes-updated', refreshMyClasses);
}, []);
const toggleMyClass = (classId: number, _name: string, _code: string) => {
const isMyClass = myClasses.includes(classId);
if (isMyClass) {
setMyClasses((prev) => prev.filter((id) => id !== classId));
apiMyClasses.remove(classId).catch(() => {});
} else {
setMyClasses((prev) => [...prev, classId]);
apiMyClasses.add(classId).catch(() => {});
}
window.dispatchEvent(new Event('my-classes-updated'));
};
const [configClass, setConfigClass] = useState<ActiveClassItem | null>(null);
const [schedules, setSchedules] = useState<ClassScheduleItem[]>([]);
const [allowedApps, setAllowedApps] = useState<string>('');
const [monitorClass, setMonitorClass] = useState<ActiveClassItem | null>(null);
const [studentsLogs, setStudentsLogs] = useState<StudentSessionLogItem[]>([]);
const [showAddClassModal, setShowAddClassModal] = useState<boolean>(false);
const [allClasses, setAllClasses] = useState<ClassItem[]>([]);
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 [appPoolOpen, setAppPoolOpen] = useState(false);
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
const addAppKeyword = (kw: string) => {
const trimmed = kw.trim().toLowerCase();
if (!trimmed) return;
setAllowedApps(prev => {
const parts = prev.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
if (parts.includes(trimmed)) return prev;
return prev ? `${prev}, ${trimmed}` : trimmed;
});
};
const applyTemplate = (keywords: string) => {
setAllowedApps((prev) => mergeKeywordCSV(prev, keywords));
};
const loadActiveClasses = async () => {
setLoading(true);
try {
const res = await apiFetchActiveSchedules();
setActiveClasses(res.data || []);
} catch (err) {
console.error('Failed to load active classes:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadActiveClasses();
}, []);
const filteredClasses = useMemo(() => {
const list = filterActiveClasses(activeClasses, searchQuery, dayFilter);
if (onlyMyClasses) {
return list.filter(c => myClasses.includes(c.rkId));
}
return list;
}, [activeClasses, searchQuery, dayFilter, onlyMyClasses, myClasses]);
const weeklyShifts = useMemo(() => countWeeklyShifts(activeClasses), [activeClasses]);
const todayClassCount = useMemo(
() => activeClasses.filter(classHasToday).length,
[activeClasses]
);
const handleOpenAddClass = async () => {
setShowAddClassModal(true);
try {
const res = await apiFetchClasses({ page: 1, pageSize: 1000 });
setAllClasses(res.data || []);
} catch (err) {
console.error('Error loading classes:', err);
}
};
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();
handleOpenConfig(cls as ActiveClassItem);
} catch (err) {
alert('Không thể kích hoạt lớp: ' + err);
}
};
const handleOpenConfig = async (cls: ActiveClassItem) => {
setConfigClass(cls);
try {
const schedRes = await apiFetchClassSchedule(cls.rkId);
setSchedules(schedRes.data || []);
const appRes = await apiFetchAllowedApps(cls.rkId);
setAllowedApps(appRes.keywords || '');
} catch (err) {
console.error('Error fetching config:', err);
}
};
const handleOpenMonitor = async (cls: ActiveClassItem) => {
setMonitorClass(cls);
setLoading(true);
try {
const logRes = await apiFetchClassSessionLogs(cls.rkId);
setStudentsLogs(logRes.data || []);
} catch (err) {
console.error('Error fetching logs:', err);
} finally {
setLoading(false);
}
};
const reloadConfigSchedules = async () => {
if (!configClass) return;
try {
const schedRes = await apiFetchClassSchedule(configClass.rkId);
setSchedules(schedRes.data || []);
loadActiveClasses();
} catch (err) {
console.error('Error reloading schedules:', err);
}
};
const handleSaveApps = async () => {
if (!configClass) return;
try {
await apiSaveAllowedApps(configClass.rkId, allowedApps);
alert('Lưu cấu hình ứng dụng được phép thành công!');
loadActiveClasses();
} catch (err) {
alert('Không thể lưu cấu hình app: ' + err);
}
};
const handleDeleteClass = async (cls: ActiveClassItem) => {
if (window.confirm(`Bạn có chắc chắn muốn hủy cấu hình và dừng giám sát lớp ${cls.name}?`)) {
try {
await apiDeleteClassSchedule(cls.rkId);
loadActiveClasses();
if (monitorClass?.rkId === cls.rkId) setMonitorClass(null);
if (configClass?.rkId === cls.rkId) setConfigClass(null);
} catch (err) {
alert('Không thể hủy kích hoạt: ' + err);
}
}
};
const handleOpenWorkspace = (cls: ActiveClassItem) => {
openClass('learning', cls.rkId, cls.name);
};
const formatSeconds = (totalSeconds: number) => {
if (!totalSeconds) return '0s';
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const parts = [];
if (hrs > 0) parts.push(`${hrs}h`);
if (mins > 0) parts.push(`${mins}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
};
const filteredAddClasses = allClasses.filter(c =>
(c.name.toLowerCase().includes(searchClassQuery.toLowerCase()) ||
c.classCode.toLowerCase().includes(searchClassQuery.toLowerCase())) &&
!activeClasses.some(ac => ac.rkId === c.rkId)
);
return (
<div className="tab-page">
<div className="tab-page-toolbar">
<div className="page-header">
<div className="page-title">
<h1>Quản lịch học & Giám sát</h1>
<p>Chọn lớp, lập lịch theo tuần giám sát sinh viên xem dạng danh sách hoặc thời khóa biểu</p>
</div>
<button className="btn btn-primary" onClick={handleOpenAddClass}>
+ Chọn lớp & Tạo lịch
</button>
</div>
</div>
<div className="tab-page-body" style={{ gap: '1rem' }}>
<div className="learning-stats-row">
<div className="learning-stat">
<span className="learning-stat-value">{activeClasses.length}</span>
<span className="learning-stat-label">Lớp đang giám sát</span>
</div>
<div className="learning-stat">
<span className="learning-stat-value">{weeklyShifts}</span>
<span className="learning-stat-label">Ca học / tuần</span>
</div>
<div className="learning-stat learning-stat--accent">
<span className="learning-stat-value">{todayClassCount}</span>
<span className="learning-stat-label">Lớp ca hôm nay</span>
</div>
<div className="learning-stat">
<span className="learning-stat-value">{filteredClasses.length}</span>
<span className="learning-stat-label">Kết quả lọc</span>
</div>
</div>
<div className="content-card learning-toolbar-card">
<div className="learning-toolbar">
<input
type="search"
className="app-pool-search learning-search"
placeholder="Tìm mã lớp, tên lớp, môn học, chuyên ngành..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
<label className="checkbox-label" style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', cursor: 'pointer', fontSize: '0.88rem', fontWeight: 600, color: 'var(--text-primary)', whiteSpace: 'nowrap', userSelect: 'none', marginLeft: '0.5rem' }}>
<input
type="checkbox"
checked={onlyMyClasses}
onChange={e => setOnlyMyClasses(e.target.checked)}
/>
<span> Lớp của tôi</span>
</label>
<div className="learning-view-toggle">
<button
type="button"
className={`learning-view-btn${viewMode === 'list' ? ' learning-view-btn--active' : ''}`}
onClick={() => setViewMode('list')}
>
Danh sách
</button>
<button
type="button"
className={`learning-view-btn${viewMode === 'timetable' ? ' learning-view-btn--active' : ''}`}
onClick={() => setViewMode('timetable')}
>
Thời khóa biểu
</button>
</div>
</div>
<div className="learning-day-filters">
<button
type="button"
className={`learning-day-chip${dayFilter === null ? ' learning-day-chip--active' : ''}`}
onClick={() => setDayFilter(null)}
>
Tất cả
</button>
{WEEK_DAYS.map((label, day) => (
<button
key={label}
type="button"
className={`learning-day-chip${dayFilter === day ? ' learning-day-chip--active' : ''}`}
onClick={() => setDayFilter(dayFilter === day ? null : day)}
>
{label}
</button>
))}
</div>
</div>
<div className="tab-page-scroll">
<div className="content-card">
{loading && (
<div className="learning-empty" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', padding: '3rem 1.5rem' }}>
<div className="sync-spinner" style={{ width: '36px', height: '36px', borderWidth: '3px' }}></div>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Đang tải dữ liệu...</p>
</div>
)}
{!loading && activeClasses.length === 0 && (
<div className="learning-empty" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', padding: '3rem 1.5rem' }}>
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', fontWeight: 500 }}>Chưa lớp nào đưc tạo lịch học.</p>
<button className="btn btn-secondary" onClick={handleOpenAddClass} style={{ marginTop: '0.5rem' }}>
Bắt đu thêm lớp đu tiên
</button>
</div>
)}
{!loading && activeClasses.length > 0 && filteredClasses.length === 0 && (
<div className="learning-empty" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', padding: '3rem 1.5rem' }}>
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', fontWeight: 500 }}>Không tìm thấy lớp phù hợp với bộ lọc hiện tại.</p>
<button
className="btn btn-secondary"
onClick={() => {
setSearchQuery('');
setDayFilter(null);
setOnlyMyClasses(false);
}}
style={{ marginTop: '0.5rem' }}
>
Xóa bộ lọc
</button>
</div>
)}
{!loading && filteredClasses.length > 0 && viewMode === 'timetable' && (
<LearningTimetable classes={filteredClasses} onOpenClass={handleOpenWorkspace} />
)}
{!loading && filteredClasses.length > 0 && viewMode === 'list' && (
<div className="learning-class-grid">
{filteredClasses.map(cls => {
const today = classHasToday(cls);
const isMyClass = myClasses.includes(cls.rkId);
return (
<article key={cls.rkId} className={`learning-class-card${today ? ' learning-class-card--today' : ''}`}>
<header className="learning-class-card-head">
<div style={{ display: 'flex', gap: '0.65rem', alignItems: 'flex-start', flex: 1, minWidth: 0 }}>
<button
type="button"
className={`btn-my-class-star ${isMyClass ? 'active' : ''}`}
onClick={(e) => {
e.stopPropagation();
toggleMyClass(cls.rkId, cls.name, cls.classCode);
}}
title={isMyClass ? 'Xóa khỏi Lớp của tôi' : 'Thêm vào Lớp của tôi'}
style={{
background: 'none',
border: 'none',
fontSize: '1.3rem',
cursor: 'pointer',
padding: 0,
lineHeight: 1,
color: isMyClass ? '#eab308' : '#d1d5db',
transition: 'color 0.2s',
marginTop: '0.1rem'
}}
>
{isMyClass ? '★' : '☆'}
</button>
<div style={{ minWidth: 0, flex: 1 }}>
<div className="learning-class-code">{cls.classCode}</div>
<h3 className="learning-class-name" title={cls.name}>{cls.name}</h3>
{(cls.specializeName || cls.systemName) && (
<p className="learning-class-meta">
{[cls.specializeName, cls.systemName].filter(Boolean).join(' · ')}
</p>
)}
</div>
</div>
<div className="learning-class-badges" style={{ flexShrink: 0 }}>
{today && <span className="learning-badge learning-badge--today">Hôm nay</span>}
{cls.studentCount != null && cls.studentCount > 0 && (
<span className="learning-badge">{cls.studentCount} SV</span>
)}
</div>
</header>
<footer className="learning-class-actions" style={{ marginTop: 'auto', paddingTop: '1rem' }}>
<button
type="button"
className="btn btn-primary btn-sm"
style={{ flex: 1 }}
onClick={() => handleOpenWorkspace(cls)}
>
Vào Workspace
</button>
<button
type="button"
className="btn btn-secondary btn-sm learning-btn-danger"
onClick={() => handleDeleteClass(cls)}
>
Hủy
</button>
</footer>
</article>
);
})}
</div>
)}
</div>
</div>
</div>
{configClass && (
<div className="modal-overlay" onClick={() => setConfigClass(null)}>
<div className="modal-container" style={{ maxWidth: '800px', width: '90%' }} onClick={e => e.stopPropagation()}>
<div className="modal-header">
<div>
<h2 className="modal-title">Cấu hình lịch học & ng dụng</h2>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
{configClass.name} ({configClass.classCode})
</p>
</div>
<button className="modal-close-btn" onClick={() => setConfigClass(null)} aria-label="Đóng">&times;</button>
</div>
<div className="modal-body" style={{ padding: '1.75rem', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
<div style={{ borderRight: '1px solid var(--border-color)', paddingRight: '1.5rem' }}>
<h4 style={{ margin: '0 0 1rem 0' }}>Ca học trong tuần</h4>
<ScheduleEditor
classId={configClass.rkId}
schedules={schedules}
onSaved={reloadConfigSchedules}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div>
<h4 style={{ margin: '0 0 1rem 0' }}>App đưc phép chạy</h4>
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', lineHeight: '1.4', marginBottom: '8px' }}>
Từ khóa ngăn chặn các ng dụng ngoài luồng trên máy sinh viên.
</p>
<textarea
rows={8}
value={allowedApps}
onChange={e => setAllowedApps(e.target.value)}
placeholder="chrome,idea64,vscode,wails,simple_care"
style={{
width: '100%',
padding: '0.75rem',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-color)',
fontFamily: 'monospace',
fontSize: '0.85rem',
color: 'var(--text-primary)',
backgroundColor: '#fff',
outline: 'none',
transition: 'var(--transition)',
resize: 'vertical',
boxSizing: 'border-box'
}}
onFocus={(e) => e.target.style.borderColor = 'var(--accent)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border-color)'}
/>
<div style={{ display: 'flex', gap: '8px', marginTop: '10px' }}>
<button
type="button"
className="btn btn-secondary btn-sm"
style={{ flex: 1 }}
onClick={() => setTemplatePickerOpen(true)}
>
Chọn khung
</button>
<button
type="button"
className="btn btn-secondary btn-sm"
style={{ flex: 1 }}
onClick={() => setAppPoolOpen(true)}
>
Mở kho
</button>
</div>
</div>
<button className="btn btn-primary" onClick={handleSaveApps} style={{ width: '100%', marginTop: '15px' }}>
Lưu whitelist app
</button>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setConfigClass(null)}>Đóng</button>
</div>
</div>
</div>
)}
{monitorClass && (
<div className="modal-overlay">
<div className="modal-container" style={{ maxWidth: '950px', width: '95%' }}>
<div className="modal-header">
<div>
<h2 className="modal-title">Nhật & giám sát sinh viên</h2>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
Lớp: <strong>{monitorClass.name}</strong> ({monitorClass.classCode})
</p>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-secondary" onClick={() => handleOpenMonitor(monitorClass)}>Tải lại</button>
<button className="btn btn-secondary close-btn" onClick={() => setMonitorClass(null)}>Đóng</button>
</div>
</div>
<div style={{ padding: '1rem 0', maxHeight: '450px', overflowY: 'auto' }}>
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Học viên</th>
<th> SV</th>
<th>Online</th>
<th>Offline</th>
<th>WiFi</th>
<th>Cập nhật</th>
<th style={{ textAlign: 'right' }}>Thao tác</th>
</tr>
</thead>
<tbody>
{studentsLogs.length === 0 ? (
<tr>
<td colSpan={7} style={{ textAlign: 'center', color: 'var(--text-muted)' }}>
Chưa dữ liệu sinh viên hôm nay.
</td>
</tr>
) : (
studentsLogs.map(log => (
<tr key={log.studentRkId}>
<td>
<div style={{ fontWeight: 700 }}>{log.fullName}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{log.email}</div>
</td>
<td><code>{log.studentCode}</code></td>
<td style={{ color: '#4caf50', fontWeight: 700 }}>{formatSeconds(log.onlineSeconds)}</td>
<td style={{ color: '#f44336', fontWeight: 700 }}>{formatSeconds(log.offlineSeconds)}</td>
<td>
<span className="course-tag">{log.wifiSsids || 'Chưa nhận'}</span>
</td>
<td>{log.lastActiveAt ? new Date(log.lastActiveAt).toLocaleTimeString('vi-VN') : '—'}</td>
<td style={{ textAlign: 'right' }}>
<button
className="btn btn-primary btn-sm"
onClick={() =>
setProctorStudent({ id: log.studentRkId, name: log.fullName, code: log.studentCode })
}
>
Xem màn hình
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
)}
{showAddClassModal && (
<div className="modal-overlay" onClick={() => setShowAddClassModal(false)}>
<div className="modal-container" style={{ maxWidth: '500px' }} onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">Chọn lớp học từ hệ thống</h2>
<button className="modal-close-btn" onClick={() => setShowAddClassModal(false)} aria-label="Đóng">&times;</button>
</div>
<div className="modal-body" style={{ padding: '1.5rem' }}>
<div className="search-input-wrapper" style={{ width: '100%', marginBottom: '1.25rem' }}>
<input
type="search"
className="search-input"
placeholder="Tìm tên lớp hoặc mã lớp..."
value={searchClassQuery}
onChange={e => setSearchClassQuery(e.target.value)}
/>
<span className="search-icon">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</span>
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{filteredAddClasses.length === 0 ? (
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '1rem 0' }}>Không tìm thấy lớp phù hợp.</p>
) : (
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{filteredAddClasses.map(c => (
<li key={c.rkId} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.85rem 1rem',
backgroundColor: 'var(--bg-app)',
border: '1px solid var(--border-color)',
borderRadius: 'var(--radius-sm)',
transition: 'var(--transition)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(187, 33, 38, 0.25)';
e.currentTarget.style.backgroundColor = '#fff';
e.currentTarget.style.boxShadow = 'var(--shadow-sm)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-color)';
e.currentTarget.style.backgroundColor = 'var(--bg-app)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<div>
<div style={{ fontWeight: 700, color: 'var(--text-primary)', fontSize: '0.9rem' }}>{c.classCode}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '2px' }}>{c.name}</div>
</div>
<button className="btn btn-primary btn-sm" onClick={() => handleActivateClass(c)} style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
Kích hoạt
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
</li>
))}
</ul>
)}
</div>
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowAddClassModal(false)}>Đóng</button>
</div>
</div>
</div>
)}
{proctorStudent && (
<LiveProctorModal
studentId={proctorStudent.id}
studentName={proctorStudent.name}
studentCode={proctorStudent.code}
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>
)}
<AppPoolModal
open={appPoolOpen}
onClose={() => setAppPoolOpen(false)}
onSelect={addAppKeyword}
allowedApps={allowedApps}
/>
<AppTemplatePickerModal
open={templatePickerOpen}
onClose={() => setTemplatePickerOpen(false)}
onSelect={applyTemplate}
allowedApps={allowedApps}
/>
</div>
);
};