diff --git a/management/src/components/LearningTab.tsx b/management/src/components/LearningTab.tsx index 374f57e..1e87857 100644 --- a/management/src/components/LearningTab.tsx +++ b/management/src/components/LearningTab.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { apiFetchActiveSchedules, apiFetchClassSchedule, @@ -18,26 +18,37 @@ import type { import { LiveProctorModal } from './LiveProctorModal'; import { openClass } from './NavHistoryBar'; import { ScheduleEditor } from './ScheduleEditor'; +import { LearningTimetable } from './LearningTimetable'; +import { + WEEK_DAYS, + filterActiveClasses, + countWeeklyShifts, + classHasToday, + schedulesByDay, + getDayLabel, + type ActiveClassItem, +} from './learningUtils'; + +type ViewMode = 'list' | 'timetable'; export const LearningTab: React.FC = () => { - const [activeClasses, setActiveClasses] = useState([]); + const [activeClasses, setActiveClasses] = useState([]); const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [dayFilter, setDayFilter] = useState(null); + const [viewMode, setViewMode] = useState('list'); - // Lớp đang cấu hình cấu trúc lịch/app (modal) - const [configClass, setConfigClass] = useState(null); + const [configClass, setConfigClass] = useState(null); const [schedules, setSchedules] = useState([]); const [allowedApps, setAllowedApps] = useState(''); - // Lớp đang xem giám sát sinh viên - const [monitorClass, setMonitorClass] = useState(null); + const [monitorClass, setMonitorClass] = useState(null); const [studentsLogs, setStudentsLogs] = useState([]); - // Trạng thái modal Thêm lớp vào lịch const [showAddClassModal, setShowAddClassModal] = useState(false); const [allClasses, setAllClasses] = useState([]); const [searchClassQuery, setSearchClassQuery] = useState(''); - // Trạng thái modal Live stream proctor const [proctorStudent, setProctorStudent] = useState<{ id: number; name: string; code: string } | null>(null); const loadActiveClasses = async () => { @@ -56,10 +67,20 @@ export const LearningTab: React.FC = () => { loadActiveClasses(); }, []); + const filteredClasses = useMemo( + () => filterActiveClasses(activeClasses, searchQuery, dayFilter), + [activeClasses, searchQuery, dayFilter] + ); + + const weeklyShifts = useMemo(() => countWeeklyShifts(activeClasses), [activeClasses]); + const todayClassCount = useMemo( + () => activeClasses.filter(classHasToday).length, + [activeClasses] + ); + const handleOpenAddClass = async () => { setShowAddClassModal(true); try { - // Tải tối đa 1000 lớp trong hệ thống để người dùng thoải mái tìm kiếm const res = await apiFetchClasses({ page: 1, pageSize: 1000 }); setAllClasses(res.data || []); } catch (err) { @@ -73,18 +94,17 @@ export const LearningTab: React.FC = () => { await apiApplyScheduleTemplate(cls.rkId); await apiSaveAllowedApps(cls.rkId, DEFAULT_ALLOWED_APPS); await loadActiveClasses(); - handleOpenConfig(cls); + handleOpenConfig(cls as ActiveClassItem); } catch (err) { alert('Không thể kích hoạt lớp: ' + err); } }; - const handleOpenConfig = async (cls: any) => { + 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) { @@ -92,7 +112,7 @@ export const LearningTab: React.FC = () => { } }; - const handleOpenMonitor = async (cls: any) => { + const handleOpenMonitor = async (cls: ActiveClassItem) => { setMonitorClass(cls); setLoading(true); try { @@ -117,6 +137,7 @@ export const LearningTab: React.FC = () => { }; 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!'); @@ -126,7 +147,7 @@ export const LearningTab: React.FC = () => { } }; - const handleDeleteClass = async (cls: any) => { + 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); @@ -139,6 +160,10 @@ export const LearningTab: React.FC = () => { } }; + const handleOpenWorkspace = (cls: ActiveClassItem) => { + openClass('learning', cls.rkId, cls.name); + }; + const formatSeconds = (totalSeconds: number) => { if (!totalSeconds) return '0s'; const hrs = Math.floor(totalSeconds / 3600); @@ -151,14 +176,9 @@ export const LearningTab: React.FC = () => { return parts.join(' '); }; - const getDayString = (day: number) => { - const days = ['Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7', 'Chủ Nhật']; - return days[day] || `Thứ ${day + 2}`; - }; - const filteredAddClasses = allClasses.filter(c => (c.name.toLowerCase().includes(searchClassQuery.toLowerCase()) || - c.classCode.toLowerCase().includes(searchClassQuery.toLowerCase())) && + c.classCode.toLowerCase().includes(searchClassQuery.toLowerCase())) && !activeClasses.some(ac => ac.rkId === c.rkId) ); @@ -168,152 +188,259 @@ export const LearningTab: React.FC = () => {

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

-

Chọn các lớp học từ hệ thống chính, tạo lịch học theo tuần và giám sát sinh viên

+

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

-
-

Lớp đang hoạt động tuần này

- {loading &&
🔄 Đang tải...
} - - {!loading && activeClasses.length === 0 ? ( -
- 📅 -

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

- +
+
+ {activeClasses.length} + Lớp đang giám sát
- ) : ( - !loading && ( -
- - - - - - - - - - - {activeClasses.map((cls) => ( - - - - - - - ))} - -
Mã Lớp / Tên LớpLịch Học Trong TuầnApp Được PhépThao Tác
-
{cls.classCode}
-
- {cls.name} -
-
-
- {cls.schedules && cls.schedules.length > 0 ? ( - cls.schedules.map((s: any, idx: number) => ( - - {getDayString(s.dayOfWeek)} Ca{s.period || 1}: {s.startTime}-{s.endTime} - {s.courseName ? ` · ${s.courseName}` : ''} - - )) - ) : ( - Chưa có lịch - )} -
-
- - {cls.allowedApps || 'Mặc định'} - - -
- - -
-
+
+ {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...

+
+ )} + + {!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 byDay = schedulesByDay(cls); + const today = classHasToday(cls); + 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 + )} +
+
+ +
+ {byDay.size === 0 ? ( +

Chưa cấu hình ca học

+ ) : ( + Array.from(byDay.entries()).map(([day, shifts]) => ( +
+ {getDayLabel(day)} +
+ {shifts.map((s, idx) => ( + + Ca{s.period || 1} {s.startTime && s.endTime ? `${s.startTime}–${s.endTime}` : ''} + {s.courseName ? ` · ${s.courseName}` : ''} + + ))} +
+
+ )) + )} +
+ + {cls.allowedApps && ( +

+ App: {cls.allowedApps} +

+ )} + +
+ + + +
+
+ ); + })} +
+ )} +
- {/* MODAL CẤU HÌNH LỊCH HỌC & APP */} {configClass && (
-

⚙️ Cấu Hình Lịch Học & Ứng Dụng

-

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

+

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

+

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

- +
- - {/* Cột trái: Quản lý Ca học */}
-

📅 Ca Học Trong Tuần

+

Ca học trong tuần

- - {/* Cột phải: Quản lý App Whitelist */}
-

🚫 App Được Phép Chạy

+

App được phép chạy

- Từ khóa ngăn chặn các ứng dụng ngoài luồng. Các ứng dụng có giao diện chứa từ khóa trong danh sách này mới được chạy trên máy sinh viên. + Từ khóa ngăn chặn các ứng dụng ngoài luồng trên máy sinh viên.