This commit is contained in:
2026-06-30 09:31:33 +07:00
parent c736326162
commit bbf8336664
77 changed files with 12601 additions and 556 deletions

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api';
import type { ClassItem, StudentItem, SyncStatus } from '../api';
import type { ClassItem, SyncStatus } from '../api';
import { openClass } from './NavHistoryBar';
export const ClassesTab: React.FC = () => {
const [classes, setClasses] = useState<ClassItem[]>([]);
@@ -9,17 +10,11 @@ export const ClassesTab: React.FC = () => {
const [pageSize] = useState(15);
const [search, setSearch] = useState('');
const [systemFilter, setSystemFilter] = useState<number | undefined>(undefined);
const [studyingOnly, setStudyingOnly] = useState(false);
const [studyingOnly, setStudyingOnly] = useState(true);
const [loading, setLoading] = useState(true);
// Sync state
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
// Modal detail class
const [activeClass, setActiveClass] = useState<ClassItem | null>(null);
const [modalStudents, setModalStudents] = useState<StudentItem[]>([]);
const [modalLoading, setModalLoading] = useState(false);
const fetchClasses = async () => {
try {
setLoading(true);
@@ -98,24 +93,10 @@ export const ClassesTab: React.FC = () => {
// Optimistic update
setClasses(prev => prev.map(c => c.rkId === cItem.rkId ? { ...c, isStudying: nextVal } : c));
await api.updateClassStudying(cItem.rkId, nextVal);
} catch (err) {
} catch (err: any) {
// Revert if error
setClasses(prev => prev.map(c => c.rkId === cItem.rkId ? { ...c, isStudying: !nextVal } : c));
alert('Không thể cập nhật trạng thái lớp học');
}
};
const handleOpenStudentsModal = async (cItem: ClassItem) => {
setActiveClass(cItem);
setModalStudents([]);
try {
setModalLoading(true);
const res = await api.getClassStudents(cItem.rkId);
setModalStudents(res.data);
} catch (err) {
alert('Không thể tải danh sách học sinh của lớp');
} finally {
setModalLoading(false);
alert(err.message || 'Không thể cập nhật trạng thái lớp học');
}
};
@@ -124,8 +105,11 @@ export const ClassesTab: React.FC = () => {
? Math.round((syncStatus.synced / syncStatus.total) * 100)
: 0;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div className="tab-page">
<div className="tab-page-toolbar">
<div className="page-header">
<div className="page-title">
<h1>Quản Lớp Học</h1>
@@ -221,9 +205,11 @@ export const ClassesTab: React.FC = () => {
</label>
</div>
</div>
</div>
{/* Classes list table */}
<div className="table-wrapper">
<div className="tab-page-body">
<div className="table-wrapper table-fill">
{loading ? (
<div className="empty-state">
<div className="sync-spinner" style={{ width: '32px', height: '32px' }}></div>
@@ -250,20 +236,26 @@ export const ClassesTab: React.FC = () => {
</thead>
<tbody>
{classes.map(cl => (
<tr key={cl.rkId}>
<tr key={cl.rkId} style={cl.isStudying ? { background: 'rgba(16, 185, 129, 0.02)' } : {}}>
<td>
<div style={{ fontWeight: 600 }}>{cl.classCode}</div>
<div style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{cl.classCode}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>ID: {cl.rkId}</div>
</td>
<td>
<div style={{ fontWeight: 500, color: 'white' }}>{cl.name}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
{cl.specializeName || '—'}
<div
style={{ fontWeight: 600, color: 'var(--accent)', fontSize: '0.95rem', cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => openClass('classes', cl.rkId, cl.name)}
title="Mở Không gian làm việc của lớp"
>
{cl.name}
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.15rem' }}>
💼 {cl.specializeName || 'Chưa có chuyên ngành'}
</div>
</td>
<td>
<span className="badge badge-muted">
{cl.systemName || `Hệ thống ${cl.systemRkId || 'Khác'}`}
<span className="badge badge-muted" style={{ fontWeight: 600 }}>
📍 {cl.systemName || `Hệ thống ${cl.systemRkId || 'Khác'}`}
</span>
</td>
<td>
@@ -280,29 +272,39 @@ export const ClassesTab: React.FC = () => {
</div>
</td>
<td>
<span className="badge badge-info">
👤 {cl.studentCount} sinh viên
<span className="badge badge-info" style={{ padding: '0.3rem 0.65rem', borderRadius: '6px', fontWeight: 600 }}>
👤 {cl.studentCount} SV
</span>
</td>
<td>
<div className="switch-container">
<label className="switch">
<input
type="checkbox"
checked={cl.isStudying}
onChange={() => handleToggleStudying(cl)}
/>
<span className="slider"></span>
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<div className="switch-container">
<label className="switch">
<input
type="checkbox"
checked={cl.isStudying}
onChange={() => handleToggleStudying(cl)}
/>
<span className="slider"></span>
</label>
</div>
{cl.isStudying ? (
<span style={{ color: 'var(--success)', fontSize: '0.72rem', display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontWeight: 700, textTransform: 'uppercase' }}>
<span className="pulse-dot-active" style={{ display: 'inline-block', width: '6px', height: '6px', background: '#10b981', borderRadius: '50%', boxShadow: '0 0 6px #10b981' }}></span>
Đang học
</span>
) : (
<span style={{ color: 'var(--text-muted)', fontSize: '0.72rem', textTransform: 'uppercase', fontWeight: 600 }}>Tạm dừng</span>
)}
</div>
</td>
<td style={{ textAlign: 'right' }}>
<button
className="btn btn-secondary"
style={{ padding: '0.45rem 0.85rem', fontSize: '0.8rem' }}
onClick={() => handleOpenStudentsModal(cl)}
style={{ padding: '0.5rem 0.95rem', fontSize: '0.8rem', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '0.35rem' }}
onClick={() => openClass('classes', cl.rkId, cl.name)}
>
Chi tiết 👥
👥 Xem lớp
</button>
</td>
</tr>
@@ -311,9 +313,11 @@ export const ClassesTab: React.FC = () => {
</table>
)}
</div>
</div>
{/* Pagination control bar */}
{!loading && classes.length > 0 && (
<div className="tab-page-footer">
<div className="pagination-row">
<div>
Hiển thị lớp thứ <b>{((page - 1) * pageSize) + 1}</b> đến <b>{Math.min(page * pageSize, total)}</b> trong tổng số <b>{total}</b> lớp học
@@ -326,7 +330,7 @@ export const ClassesTab: React.FC = () => {
>
</button>
<span style={{ display: 'flex', alignItems: 'center', padding: '0 1rem', fontWeight: 600, color: 'white' }}>
<span style={{ display: 'flex', alignItems: 'center', padding: '0 1rem', fontWeight: 600, color: 'var(--text-primary)' }}>
Trang {page} / {Math.ceil(total / pageSize) || 1}
</span>
<button
@@ -338,91 +342,6 @@ export const ClassesTab: React.FC = () => {
</button>
</div>
</div>
)}
{/* Students roster modal */}
{activeClass && (
<div className="modal-overlay" onClick={() => setActiveClass(null)}>
<div className="modal-container" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<div className="modal-title">
<h3>Danh sách sinh viên lớp: {activeClass.name}</h3>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.2rem' }}>
lớp: {activeClass.classCode} | số: {activeClass.studentCount}
</p>
</div>
<button className="modal-close-btn" onClick={() => setActiveClass(null)}>
&times;
</button>
</div>
<div className="modal-body">
<div className="detail-grid">
<div>
<div className="detail-item-label">Chuyên Ngành</div>
<div className="detail-item-value">{activeClass.specializeName || 'Chưa phân phối'}</div>
</div>
<div>
<div className="detail-item-label">Chi Nhánh / Phân Hệ</div>
<div className="detail-item-value">{activeClass.systemName || 'Chung'}</div>
</div>
<div>
<div className="detail-item-label">Loại Lớp</div>
<div className="detail-item-value" style={{ textTransform: 'capitalize' }}>{activeClass.type || 'Chính thức'}</div>
</div>
</div>
{modalLoading ? (
<div className="empty-state" style={{ minHeight: '200px' }}>
<div className="sync-spinner"></div>
<p style={{ marginTop: '0.5rem' }}>Đang tải danh sách học sinh...</p>
</div>
) : modalStudents.length === 0 ? (
<div className="empty-state" style={{ minHeight: '200px' }}>
<div className="empty-state-icon">👥</div>
<h3>Lớp trống hoặc chưa đng bộ danh sách</h3>
<p style={{ fontSize: '0.85rem', maxWidth: '380px', margin: '0 auto' }}>
Hãy kiểm tra xem lớp gán môn học nào hoạt đng không. Danh sách sinh viên đưc kéo từ kết quả học tập của lớp.
</p>
</div>
) : (
<table className="data-table" style={{ fontSize: '0.85rem' }}>
<thead>
<tr>
<th> SV</th>
<th>Họ Tên</th>
<th>Email</th>
<th>Số Điện Thoại</th>
<th>Vị Trí</th>
<th>Trạng Thái</th>
</tr>
</thead>
<tbody>
{modalStudents.map(st => (
<tr key={st.rkId}>
<td style={{ fontWeight: 600 }}>{st.studentCode}</td>
<td style={{ color: 'white', fontWeight: 500 }}>{st.fullName}</td>
<td>{st.email}</td>
<td>{st.phone || '—'}</td>
<td>{st.location || '—'}</td>
<td>
<span className={`badge ${st.status === 'Đang học' || st.status === 'active' ? 'badge-success' : 'badge-muted'}`}>
{st.status || 'Đang học'}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setActiveClass(null)}>
Đóng
</button>
</div>
</div>
</div>
)}
</div>