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([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [dayFilter, setDayFilter] = useState(null); const [viewMode, setViewMode] = useState('list'); const [onlyMyClasses, setOnlyMyClasses] = useState(false); const [myClasses, setMyClasses] = useState([]); 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(null); const [schedules, setSchedules] = useState([]); const [allowedApps, setAllowedApps] = useState(''); const [monitorClass, setMonitorClass] = useState(null); const [studentsLogs, setStudentsLogs] = useState([]); const [showAddClassModal, setShowAddClassModal] = useState(false); const [allClasses, setAllClasses] = useState([]); const [searchClassQuery, setSearchClassQuery] = useState(''); 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 (

Quản lý lịch học & Giám sát

Chọn lớp, lập lịch theo tuần và giám sát sinh viên — xem dạng danh sách hoặc thời khóa biểu

{activeClasses.length} Lớp đang giám sát
{weeklyShifts} Ca học / tuần
{todayClassCount} Lớp có ca hôm nay
{filteredClasses.length} Kết quả lọc
setSearchQuery(e.target.value)} />
{WEEK_DAYS.map((label, day) => ( ))}
{loading && (

Đang tải dữ liệu...

)} {!loading && activeClasses.length === 0 && (

Chưa có lớp nào được tạo lịch học.

)} {!loading && activeClasses.length > 0 && filteredClasses.length === 0 && (

Không tìm thấy lớp phù hợp với bộ lọc hiện tại.

)} {!loading && filteredClasses.length > 0 && viewMode === 'timetable' && ( )} {!loading && filteredClasses.length > 0 && viewMode === 'list' && (
{filteredClasses.map(cls => { const today = classHasToday(cls); const isMyClass = myClasses.includes(cls.rkId); return (
{cls.classCode}

{cls.name}

{(cls.specializeName || cls.systemName) && (

{[cls.specializeName, cls.systemName].filter(Boolean).join(' · ')}

)}
{today && Hôm nay} {cls.studentCount != null && cls.studentCount > 0 && ( {cls.studentCount} SV )}
); })}
)}
{configClass && (
setConfigClass(null)}>
e.stopPropagation()}>

Cấu hình lịch học & ứng dụng

{configClass.name} ({configClass.classCode})

Ca học trong tuần

App được phép chạy

Từ khóa ngăn chặn các ứng dụng ngoài luồng trên máy sinh viên.