up
Some checks failed
Deploy on Master Change / deploy (push) Failing after 1m23s

This commit is contained in:
2026-07-20 12:38:02 +07:00
parent 6ee9773f7f
commit 232f9873a0
8 changed files with 516 additions and 121 deletions

View File

@@ -32,9 +32,12 @@ function localInputToISO(v: string) {
return new Date(v).toISOString();
}
type RoomScope = 'mine' | 'all';
export const ExamsTab: React.FC = () => {
const { staff } = useAuth();
const isSuperAdmin = staff?.email === 'phuocntb@rikkeiacademy.com';
const myStaffId = staff?.id ?? 0;
const [rooms, setRooms] = useState<ExamRoomItem[]>([]);
const [loading, setLoading] = useState(true);
@@ -44,10 +47,12 @@ export const ExamsTab: React.FC = () => {
const [end, setEnd] = useState('');
const [busy, setBusy] = useState(false);
const [search, setSearch] = useState('');
const [roomScope, setRoomScope] = useState<RoomScope>('mine');
const load = async () => {
setLoading(true);
try {
// Luôn lấy đủ danh sách để toggle mine/all mượt; lọc phía client theo createdByStaffId
const res = await apiExam.list();
setRooms(res.data);
} catch (e) {
@@ -61,20 +66,28 @@ export const ExamsTab: React.FC = () => {
load();
}, []);
const scopedRooms = useMemo(() => {
if (roomScope === 'all' || !myStaffId) return rooms;
return rooms.filter((r) => Number(r.createdByStaffId) === myStaffId);
}, [rooms, roomScope, myStaffId]);
const stats = useMemo(() => {
const active = rooms.filter((r) => (r.displayStatus || r.status) === 'active').length;
const upcoming = rooms.filter((r) => {
const active = scopedRooms.filter((r) => (r.displayStatus || r.status) === 'active').length;
const upcoming = scopedRooms.filter((r) => {
const st = r.displayStatus || r.status;
return st === 'ready' || st === 'draft';
}).length;
return { total: rooms.length, active, upcoming };
}, [rooms]);
return { total: scopedRooms.length, active, upcoming };
}, [scopedRooms]);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return rooms;
return rooms.filter((r) => r.name.toLowerCase().includes(q));
}, [rooms, search]);
if (!q) return scopedRooms;
return scopedRooms.filter((r) => r.name.toLowerCase().includes(q));
}, [scopedRooms, search]);
const canDeleteRoom = (r: ExamRoomItem) =>
isSuperAdmin || (myStaffId > 0 && Number(r.createdByStaffId) === myStaffId);
const create = async () => {
if (!name.trim() || !start || !end) return;
@@ -115,7 +128,11 @@ export const ExamsTab: React.FC = () => {
<header className="page-header page-header--row">
<div>
<h1 className="page-title">Danh sách phòng thi</h1>
<p className="page-desc">Tạo phòng, chia đ ngẫu nhiên, gửi đ thu bài từ sinh viên.</p>
<p className="page-desc">
{roomScope === 'mine'
? 'Mặc định chỉ hiện phòng thi do bạn tạo — chuyển sang xem tất cả khi cần'
: 'Đang xem toàn bộ phòng thi trong hệ thống'}
</p>
</div>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
+ Tạo phòng thi
@@ -125,7 +142,7 @@ export const ExamsTab: React.FC = () => {
<div className="exams-stats-row">
<div className="learning-stat">
<span className="learning-stat-value">{stats.total}</span>
<span className="learning-stat-label">Tổng phòng</span>
<span className="learning-stat-label">{roomScope === 'mine' ? 'Phòng của tôi' : 'Tổng phòng'}</span>
</div>
<div className="learning-stat learning-stat--accent">
<span className="learning-stat-value">{stats.active}</span>
@@ -137,13 +154,30 @@ export const ExamsTab: React.FC = () => {
</div>
</div>
<div className="exams-toolbar">
<div className="exams-toolbar" style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="learning-view-toggle">
<button
type="button"
className={`learning-view-btn${roomScope === 'mine' ? ' learning-view-btn--active' : ''}`}
onClick={() => setRoomScope('mine')}
>
Phòng của tôi
</button>
<button
type="button"
className={`learning-view-btn${roomScope === 'all' ? ' learning-view-btn--active' : ''}`}
onClick={() => setRoomScope('all')}
>
Xem tất cả
</button>
</div>
<input
type="search"
className="app-pool-search exams-search"
placeholder="Tìm theo tên phòng thi..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ flex: 1, minWidth: 200 }}
/>
</div>
@@ -203,18 +237,38 @@ export const ExamsTab: React.FC = () => {
{loading ? (
<div className="system-empty">Đang tải...</div>
) : filtered.length === 0 ? (
<div className="system-empty">
{search.trim() ? 'Không tìm thấy phòng thi phù hợp.' : 'Chưa có phòng thi — bấm Tạo phòng thi để bắt đầu.'}
<div className="system-empty" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem' }}>
<p style={{ margin: 0 }}>
{search.trim()
? 'Không tìm thấy phòng thi phù hợp.'
: roomScope === 'mine'
? 'Bạn chưa tạo phòng thi nào.'
: 'Chưa có phòng thi — bấm Tạo phòng thi để bắt đầu.'}
</p>
{!search.trim() && roomScope === 'mine' && (
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
+ Tạo phòng thi
</button>
<button type="button" className="btn btn-secondary" onClick={() => setRoomScope('all')}>
Xem tất cả
</button>
</div>
)}
</div>
) : (
<div className="exams-card-grid">
{filtered.map((r) => {
const st = statusLabel(r.displayStatus || r.status);
const mine = myStaffId > 0 && Number(r.createdByStaffId) === myStaffId;
return (
<article key={r.id} className="exam-list-card">
<div className="exam-list-card-head">
<h3>{r.name}</h3>
<span className={`badge ${st.cls}`}>{st.text}</span>
<div style={{ display: 'flex', gap: '0.35rem', alignItems: 'center', flexWrap: 'wrap' }}>
{mine && <span className="badge badge-warning" style={{ fontSize: '0.7rem' }}>Của tôi</span>}
<span className={`badge ${st.cls}`}>{st.text}</span>
</div>
</div>
<div className="exam-list-card-meta">
<div>
@@ -232,7 +286,7 @@ export const ExamsTab: React.FC = () => {
<button type="button" className="btn btn-primary btn-sm exam-list-card-open" onClick={() => openExam(r.id, r.name)} style={{ flex: 1, margin: 0 }}>
Mở phòng thi
</button>
{isSuperAdmin && (
{canDeleteRoom(r) && (
<button
type="button"
className="btn btn-secondary btn-sm"
@@ -249,14 +303,6 @@ export const ExamsTab: React.FC = () => {
transition: 'var(--transition)',
margin: 0
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--danger)';
e.currentTarget.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--danger)';
}}
>
Xóa
</button>