This commit is contained in:
2026-06-30 15:53:01 +07:00
parent 595aebd8a5
commit 74b540cac2
19 changed files with 1719 additions and 252 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { apiExam, type ExamRoomItem } from '../api';
import { openExam } from './NavHistoryBar';
@@ -39,6 +39,7 @@ export const ExamsTab: React.FC = () => {
const [start, setStart] = useState('');
const [end, setEnd] = useState('');
const [busy, setBusy] = useState(false);
const [search, setSearch] = useState('');
const load = async () => {
setLoading(true);
@@ -52,7 +53,24 @@ export const ExamsTab: React.FC = () => {
}
};
useEffect(() => { load(); }, []);
useEffect(() => {
load();
}, []);
const stats = useMemo(() => {
const active = rooms.filter((r) => (r.displayStatus || r.status) === 'active').length;
const upcoming = rooms.filter((r) => {
const st = r.displayStatus || r.status;
return st === 'ready' || st === 'draft';
}).length;
return { total: rooms.length, active, upcoming };
}, [rooms]);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return rooms;
return rooms.filter((r) => r.name.toLowerCase().includes(q));
}, [rooms, search]);
const create = async () => {
if (!name.trim() || !start || !end) return;
@@ -65,6 +83,9 @@ export const ExamsTab: React.FC = () => {
});
setShowCreate(false);
setName('');
setStart('');
setEnd('');
await load();
openExam(room.id, room.name);
} catch (e: any) {
alert(e?.message || 'Lỗi');
@@ -74,20 +95,47 @@ export const ExamsTab: React.FC = () => {
};
return (
<div className="page-stack">
<div className="tab-page exams-page">
<header className="page-header page-header--row">
<div>
<h1 className="page-title">Phòng thi</h1>
<p className="page-desc">Tạo phòng thi, chia đ ngẫu nhiên, gửi đ thu bài từ sinh viên.</p>
<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>
</div>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>+ Tạo phòng thi</button>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
+ Tạo phòng thi
</button>
</header>
<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>
</div>
<div className="learning-stat learning-stat--accent">
<span className="learning-stat-value">{stats.active}</span>
<span className="learning-stat-label">Đang thi</span>
</div>
<div className="learning-stat">
<span className="learning-stat-value">{stats.upcoming}</span>
<span className="learning-stat-label">Sắp / tạm</span>
</div>
</div>
<div className="exams-toolbar">
<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)}
/>
</div>
{showCreate && (
<div className="card" style={{ padding: '1.25rem' }}>
<h2 className="section-title">Phòng thi mới</h2>
<div className="form-grid" style={{ maxWidth: 520 }}>
<label className="login-field">
<div className="system-form-card exams-create-card">
<h2 className="system-form-title">Phòng thi mới</h2>
<div className="system-form-grid">
<label className="login-field system-form-full">
<span>Tên phòng thi</span>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="VD: Thi cuối kỳ Java" />
</label>
@@ -100,48 +148,56 @@ export const ExamsTab: React.FC = () => {
<input type="datetime-local" value={end} onChange={(e) => setEnd(e.target.value)} />
</label>
</div>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
<button type="button" className="btn btn-primary" disabled={busy} onClick={create}>Tạo</button>
<button type="button" className="btn btn-ghost" onClick={() => setShowCreate(false)}>Hủy</button>
<div className="system-form-actions">
<div />
<div className="system-form-actions-right">
<button type="button" className="btn btn-ghost" onClick={() => setShowCreate(false)}>
Hủy
</button>
<button type="button" className="btn btn-primary" disabled={busy} onClick={create}>
{busy ? 'Đang tạo...' : 'Tạo & mở phòng'}
</button>
</div>
</div>
</div>
)}
<div className="card table-card">
{loading ? (
<div className="table-empty">Đang tải...</div>
) : rooms.length === 0 ? (
<div className="table-empty">Chưa phòng thi nào.</div>
) : (
<table className="data-table">
<thead>
<tr>
<th>Tên</th>
<th>Thời gian</th>
<th>SV / Đ</th>
<th>Trạng thái</th>
<th></th>
</tr>
</thead>
<tbody>
{rooms.map((r) => {
const st = statusLabel(r.displayStatus || r.status);
return (
<tr key={r.id}>
<td><strong>{r.name}</strong></td>
<td style={{ fontSize: '0.82rem' }}>{fmtTime(r.startTime)} {fmtTime(r.endTime)}</td>
<td>{r.studentCount} SV · {r.paperCount} đ</td>
<td><span className={`badge ${st.cls}`}>{st.text}</span></td>
<td>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => openExam(r.id, r.name)}>Mở</button>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
{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>
) : (
<div className="exams-card-grid">
{filtered.map((r) => {
const st = statusLabel(r.displayStatus || r.status);
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>
<div className="exam-list-card-meta">
<div>
<span className="exam-list-card-label">Thời gian</span>
<span>{fmtTime(r.startTime)}</span>
<span className="exam-list-card-sep"></span>
<span>{fmtTime(r.endTime)}</span>
</div>
<div>
<span className="exam-list-card-label">Quy </span>
<span>{r.studentCount} sinh viên · {r.paperCount} đ</span>
</div>
</div>
<button type="button" className="btn btn-primary btn-sm exam-list-card-open" onClick={() => openExam(r.id, r.name)}>
Mở phòng thi
</button>
</article>
);
})}
</div>
)}
</div>
);
};