This commit is contained in:
2026-06-29 23:45:08 +07:00
parent 5290eeeb91
commit c736326162
15 changed files with 3067 additions and 388 deletions

View File

@@ -0,0 +1,430 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api';
import type { ClassItem, StudentItem, SyncStatus } from '../api';
export const ClassesTab: React.FC = () => {
const [classes, setClasses] = useState<ClassItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(15);
const [search, setSearch] = useState('');
const [systemFilter, setSystemFilter] = useState<number | undefined>(undefined);
const [studyingOnly, setStudyingOnly] = useState(false);
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);
const res = await api.getClasses({
page,
pageSize,
q: search || undefined,
systemRkId: systemFilter,
studyingOnly,
});
setClasses(res.data);
setTotal(res.total);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const fetchSyncStatus = async () => {
try {
const status = await api.getClassesSyncStatus();
setSyncStatus(status);
return status.running;
} catch (err) {
console.error('Lỗi khi lấy status sync:', err);
return false;
}
};
useEffect(() => {
fetchClasses();
}, [page, search, systemFilter, studyingOnly]);
// Check sync status on load and start polling if running
useEffect(() => {
fetchSyncStatus();
}, []);
// Sync polling logic
useEffect(() => {
let timer: any;
if (syncStatus?.running) {
timer = setInterval(async () => {
const isRunning = await fetchSyncStatus();
if (!isRunning) {
clearInterval(timer);
fetchClasses(); // Reload data when sync completes
}
}, 2000);
}
return () => {
if (timer) clearInterval(timer);
};
}, [syncStatus?.running]);
const handleStartSync = async () => {
try {
await api.startClassesSync();
// Set local state to running to trigger useEffect poller
setSyncStatus({
running: true,
done: false,
total: 0,
synced: 0,
updatedAt: Date.now() / 1000
});
} catch (err: any) {
alert(err.message || 'Không thể bắt đầu đồng bộ lớp học');
}
};
const handleToggleStudying = async (cItem: ClassItem) => {
const nextVal = !cItem.isStudying;
try {
// Optimistic update
setClasses(prev => prev.map(c => c.rkId === cItem.rkId ? { ...c, isStudying: nextVal } : c));
await api.updateClassStudying(cItem.rkId, nextVal);
} catch (err) {
// 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);
}
};
// Tính toán % tiến trình sync
const syncPercent = syncStatus && syncStatus.total > 0
? Math.round((syncStatus.synced / syncStatus.total) * 100)
: 0;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div className="page-header">
<div className="page-title">
<h1>Quản Lớp Học</h1>
<p>Đng bộ dữ liệu lớp học sinh viên từ hệ thống chính</p>
</div>
<button
className="btn btn-primary"
onClick={handleStartSync}
disabled={syncStatus?.running}
>
{syncStatus?.running ? (
<>
<div className="sync-spinner"></div> Đng bộ...
</>
) : (
'🔄 Đồng bộ Lớp học'
)}
</button>
</div>
{/* Sync Status Banner */}
{syncStatus && (syncStatus.running || syncStatus.done || syncStatus.error) && (
<div className="sync-progress-banner">
<div className="sync-header">
<div className="sync-title">
{syncStatus.running && <div className="sync-spinner"></div>}
<span>
{syncStatus.running && `Đang đồng bộ lớp học... (${syncPercent}%)`}
{syncStatus.done && '🎉 Đồng bộ lớp học hoàn tất!'}
{syncStatus.error && `❌ Đồng bộ thất bại: ${syncStatus.error}`}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
Đã đng bộ: {syncStatus.synced} / {syncStatus.total} lớp
</div>
</div>
{syncStatus.running && (
<div className="sync-bar-container">
<div className="sync-bar" style={{ width: `${syncPercent}%` }}></div>
</div>
)}
<div className="sync-meta">
<span>Cập nhật lần cuối: {new Date(syncStatus.updatedAt * 1000).toLocaleString()}</span>
{syncStatus.running && <span style={{ color: 'var(--accent-hover)' }}>Vui lòng không đóng trình duyệt</span>}
</div>
</div>
)}
{/* Control bar filters */}
<div className="control-bar">
<div className="search-input-wrapper">
<input
type="text"
className="search-input"
placeholder="Tìm theo tên, mã lớp, chuyên ngành..."
value={search}
onChange={e => {
setSearch(e.target.value);
setPage(1);
}}
/>
<span className="search-icon">🔍</span>
</div>
<div className="filters-wrapper">
<select
className="select-filter"
value={systemFilter === undefined ? '' : systemFilter}
onChange={e => {
const val = e.target.value;
setSystemFilter(val === '' ? undefined : Number(val));
setPage(1);
}}
>
<option value="">Tất cả phân hệ</option>
<option value="1"> Nội (System 1)</option>
<option value="2">Hồ Chí Minh (System 2)</option>
<option value="3">Đà Nẵng (System 3)</option>
<option value="4">Cần Thơ (System 4)</option>
</select>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.9rem' }}>
<input
type="checkbox"
checked={studyingOnly}
onChange={e => {
setStudyingOnly(e.target.checked);
setPage(1);
}}
style={{ width: '16px', height: '16px', accentColor: 'var(--accent)' }}
/>
Chỉ xem lớp đang học
</label>
</div>
</div>
{/* Classes list table */}
<div className="table-wrapper">
{loading ? (
<div className="empty-state">
<div className="sync-spinner" style={{ width: '32px', height: '32px' }}></div>
<p style={{ marginTop: '0.5rem' }}>Đang tải danh sách lớp học...</p>
</div>
) : classes.length === 0 ? (
<div className="empty-state">
<div className="empty-state-icon">🏫</div>
<h2>Không tìm thấy lớp học nào</h2>
<p>Hãy thử thay đi bộ lọc hoặc bấm nút "Đồng bộ Lớp học" đ kéo dữ liệu mới.</p>
</div>
) : (
<table className="data-table">
<thead>
<tr>
<th> lớp / ID</th>
<th>Tên Lớp học</th>
<th>Phân hệ</th>
<th>Môn học của lớp</th>
<th>Sinh viên</th>
<th>Đang dạy</th>
<th style={{ textAlign: 'right' }}>Hành đng</th>
</tr>
</thead>
<tbody>
{classes.map(cl => (
<tr key={cl.rkId}>
<td>
<div style={{ fontWeight: 600 }}>{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>
</td>
<td>
<span className="badge badge-muted">
{cl.systemName || `Hệ thống ${cl.systemRkId || 'Khác'}`}
</span>
</td>
<td>
<div className="courses-tag-list">
{cl.courses && cl.courses.length > 0 ? (
cl.courses.map((co, i) => (
<span key={i} className="course-tag">
{co.courseName}
</span>
))
) : (
<span style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>Chưa môn học</span>
)}
</div>
</td>
<td>
<span className="badge badge-info">
👤 {cl.studentCount} sinh viên
</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>
</td>
<td style={{ textAlign: 'right' }}>
<button
className="btn btn-secondary"
style={{ padding: '0.45rem 0.85rem', fontSize: '0.8rem' }}
onClick={() => handleOpenStudentsModal(cl)}
>
Chi tiết 👥
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Pagination control bar */}
{!loading && classes.length > 0 && (
<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
</div>
<div className="pagination-btn-group">
<button
className="pagination-btn"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
</button>
<span style={{ display: 'flex', alignItems: 'center', padding: '0 1rem', fontWeight: 600, color: 'white' }}>
Trang {page} / {Math.ceil(total / pageSize) || 1}
</span>
<button
className="pagination-btn"
onClick={() => setPage(p => p + 1)}
disabled={page >= Math.ceil(total / pageSize)}
>
</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>
);
};

View File

@@ -0,0 +1,137 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api';
import type { StatsResponse } from '../api';
interface DashboardTabProps {
onNavigate: (tab: string) => void;
}
export const DashboardTab: React.FC<DashboardTabProps> = ({ onNavigate }) => {
const [stats, setStats] = useState<StatsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStats = async () => {
try {
setLoading(true);
const data = await api.getStats();
setStats(data);
} catch (err: any) {
setError(err.message || 'Không thể tải dữ liệu thống kê');
} finally {
setLoading(false);
}
};
fetchStats();
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div className="page-header">
<div className="page-title">
<h1>Tổng Quan Hệ Thống</h1>
<p>Giám sát đng bộ quản lớp học, sinh viên từ hệ thống chính</p>
</div>
</div>
{loading && (
<div className="empty-state">
<div className="sync-spinner" style={{ width: '40px', height: '40px', borderWidth: '3px' }}></div>
<p style={{ marginTop: '1rem' }}>Đang tải thông số hệ thống...</p>
</div>
)}
{error && (
<div className="empty-state" style={{ border: '1px solid rgba(239, 68, 68, 0.2)', backgroundColor: 'rgba(239, 68, 68, 0.05)' }}>
<div className="empty-state-icon" style={{ color: 'var(--danger)' }}></div>
<p style={{ color: 'var(--danger)', fontWeight: 600 }}>Lỗi: {error}</p>
</div>
)}
{!loading && !error && stats && (
<>
<div className="stats-grid">
<div className="stat-card" onClick={() => onNavigate('classes')} style={{ cursor: 'pointer' }}>
<div>
<div className="stat-label">Tổng lớp học đã đng bộ</div>
<div className="stat-value">{stats.totalClasses}</div>
</div>
<div className="stat-icon">🏫</div>
</div>
<div className="stat-card" onClick={() => onNavigate('classes')} style={{ cursor: 'pointer' }}>
<div>
<div className="stat-label">Lớp đang giảng dạy</div>
<div className="stat-value" style={{ color: 'var(--success)' }}>{stats.activeClasses}</div>
</div>
<div className="stat-icon" style={{ color: 'var(--success)' }}>🟢</div>
</div>
<div className="stat-card" onClick={() => onNavigate('students')} style={{ cursor: 'pointer' }}>
<div>
<div className="stat-label">Tổng sinh viên đã đng bộ</div>
<div className="stat-value" style={{ color: 'var(--accent-hover)' }}>{stats.totalStudents}</div>
</div>
<div className="stat-icon">🎓</div>
</div>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))',
gap: '1.5rem',
marginTop: '1rem'
}}>
<div style={{
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '2rem',
borderRadius: '18px',
backdropFilter: 'blur(10px)'
}}>
<h2 style={{ marginBottom: '1rem', color: 'white', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
🚀 Khởi đng nhanh
</h2>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', lineHeight: '1.6', marginBottom: '1.5rem' }}>
Chào mừng bạn đến với trang quản trị <b>Simple Care</b>.
Hệ thống hỗ trợ đng bộ hóa thông tin tự đng từ cổng đào tạo chính (QLĐT), giúp lưu trữ, cập nhật trạng thái lớp học danh sách sinh viên nội bộ không lo nh hưởng đến dữ liệu hiện .
</p>
<div style={{ display: 'flex', gap: '1rem' }}>
<button className="btn btn-primary" onClick={() => onNavigate('classes')}>
Quản Lớp học
</button>
<button className="btn btn-secondary" onClick={() => onNavigate('students')}>
Danh sách Sinh viên
</button>
</div>
</div>
<div style={{
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '2rem',
borderRadius: '18px',
backdropFilter: 'blur(10px)'
}}>
<h2 style={{ marginBottom: '1rem', color: 'white' }}>📋 Quy trình hoạt đng</h2>
<ul style={{
color: 'var(--text-secondary)',
fontSize: '0.95rem',
lineHeight: '1.8',
paddingLeft: '1.25rem',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem'
}}>
<li><b>Đng bộ Lớp học:</b> Tải toàn bộ danh sách lớp học các môn học tương ng. Đng thời kéo thông tin sinh viên thuộc từng lớp thông qua bảng kết quả lớp học.</li>
<li><b>Quản Lớp:</b> Lọc danh sách lớp theo Phân hệ (System ID), Tìm kiếm tên hoặc lớp Bật/Tắt cờ <b>Đang học</b>.</li>
<li><b>Đng bộ Sinh viên:</b> Thực hiện kéo toàn bộ sinh viên trên hệ thống chính (kèm thông tin liên hệ, hệ học) lưu trữ vào Database phục vụ cho học tập thi cử.</li>
</ul>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,248 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api';
import type { StudentItem, SyncStatus } from '../api';
export const StudentsTab: React.FC = () => {
const [students, setStudents] = useState<StudentItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(15);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
// Sync state
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
const fetchStudents = async () => {
try {
setLoading(true);
const res = await api.getStudents({
page,
pageSize,
q: search || undefined,
});
setStudents(res.data);
setTotal(res.total);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const fetchSyncStatus = async () => {
try {
const status = await api.getStudentsSyncStatus();
setSyncStatus(status);
return status.running;
} catch (err) {
console.error('Lỗi khi lấy status sync:', err);
return false;
}
};
useEffect(() => {
fetchStudents();
}, [page, search]);
useEffect(() => {
fetchSyncStatus();
}, []);
// Sync polling logic
useEffect(() => {
let timer: any;
if (syncStatus?.running) {
timer = setInterval(async () => {
const isRunning = await fetchSyncStatus();
if (!isRunning) {
clearInterval(timer);
fetchStudents(); // Reload data when sync completes
}
}, 2000);
}
return () => {
if (timer) clearInterval(timer);
};
}, [syncStatus?.running]);
const handleStartSync = async () => {
try {
await api.startStudentsSync();
// Set local state to running to trigger useEffect poller
setSyncStatus({
running: true,
done: false,
total: 0,
synced: 0,
updatedAt: Date.now() / 1000
});
} catch (err: any) {
alert(err.message || 'Không thể bắt đầu đồng bộ sinh viên');
}
};
// Tính toán % tiến trình sync
const syncPercent = syncStatus && syncStatus.total > 0
? Math.round((syncStatus.synced / syncStatus.total) * 100)
: 0;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div className="page-header">
<div className="page-title">
<h1>Danh Sách Sinh Viên</h1>
<p>Danh sách toàn bộ sinh viên trong hệ thống đưc đng bộ</p>
</div>
<button
className="btn btn-primary"
onClick={handleStartSync}
disabled={syncStatus?.running}
>
{syncStatus?.running ? (
<>
<div className="sync-spinner"></div> Đng bộ...
</>
) : (
'🔄 Đồng bộ toàn bộ SV'
)}
</button>
</div>
{/* Sync Status Banner */}
{syncStatus && (syncStatus.running || syncStatus.done || syncStatus.error) && (
<div className="sync-progress-banner" style={{ borderStyle: 'solid', borderColor: 'var(--success)' }}>
<div className="sync-header">
<div className="sync-title">
{syncStatus.running && <div className="sync-spinner"></div>}
<span>
{syncStatus.running && `Đang tải sinh viên... Trang ${syncStatus.page || 0} (${syncPercent}%)`}
{syncStatus.done && '🎉 Đồng bộ toàn bộ sinh viên hoàn tất!'}
{syncStatus.error && `❌ Đồng bộ thất bại: ${syncStatus.error}`}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
Đã tải: {syncStatus.synced} / {syncStatus.total} sinh viên
</div>
</div>
{syncStatus.running && (
<div className="sync-bar-container">
<div className="sync-bar" style={{ width: `${syncPercent}%`, background: 'linear-gradient(to right, var(--success), #a7f3d0)' }}></div>
</div>
)}
<div className="sync-meta">
<span>Cập nhật lần cuối: {new Date(syncStatus.updatedAt * 1000).toLocaleString()}</span>
{syncStatus.running && <span style={{ color: 'var(--success)' }}>Hệ thống đang kéo dữ liệu trang {syncStatus.page}...</span>}
</div>
</div>
)}
{/* Control filters */}
<div className="control-bar">
<div className="search-input-wrapper" style={{ maxWidth: '400px' }}>
<input
type="text"
className="search-input"
placeholder="Tìm theo tên, mã SV, email, số điện thoại..."
value={search}
onChange={e => {
setSearch(e.target.value);
setPage(1);
}}
/>
<span className="search-icon">🔍</span>
</div>
</div>
{/* Students Table */}
<div className="table-wrapper">
{loading ? (
<div className="empty-state">
<div className="sync-spinner" style={{ width: '32px', height: '32px' }}></div>
<p style={{ marginTop: '0.5rem' }}>Đang tải danh sách sinh viên...</p>
</div>
) : students.length === 0 ? (
<div className="empty-state">
<div className="empty-state-icon">👥</div>
<h2>Không sinh viên nào</h2>
<p>Hãy thử thay đi bộ lọc hoặc bấm nút "Đồng bộ toàn bộ SV" đ tải dữ liệu.</p>
</div>
) : (
<table className="data-table">
<thead>
<tr>
<th> SV</th>
<th>Họ Tên</th>
<th>Thông Tin Liên Hệ</th>
<th>Ngày sinh / Giới tính</th>
<th>Phân hệ</th>
<th>Đa điểm</th>
<th>Trạng thái</th>
</tr>
</thead>
<tbody>
{students.map(st => (
<tr key={st.id}>
<td style={{ fontWeight: 600 }}>{st.studentCode}</td>
<td style={{ color: 'white', fontWeight: 500 }}>
{st.fullName}
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>ID: {st.rkId}</div>
</td>
<td>
<div>📧 {st.email}</div>
{st.phone && <div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>📞 {st.phone}</div>}
</td>
<td>
<div>🎂 {st.dateOfBirth ? new Date(st.dateOfBirth).toLocaleDateString('vi-VN') : '—'}</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
Giới tính: {st.gender === 1 ? 'Nam' : st.gender === 0 ? 'Nữ' : 'Khác'}
</div>
</td>
<td>
<span className="badge badge-muted">
{st.systemName || 'Chung'}
</span>
</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>
{/* Pagination controls */}
{!loading && students.length > 0 && (
<div className="pagination-row">
<div>
Hiển thị sinh viên thứ <b>{((page - 1) * pageSize) + 1}</b> đến <b>{Math.min(page * pageSize, total)}</b> trong tổng số <b>{total}</b> sinh viên
</div>
<div className="pagination-btn-group">
<button
className="pagination-btn"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
</button>
<span style={{ display: 'flex', alignItems: 'center', padding: '0 1rem', fontWeight: 600, color: 'white' }}>
Trang {page} / {Math.ceil(total / pageSize) || 1}
</span>
<button
className="pagination-btn"
onClick={() => setPage(p => p + 1)}
disabled={page >= Math.ceil(total / pageSize)}
>
</button>
</div>
</div>
)}
</div>
);
};