This commit is contained in:
2026-06-30 11:51:34 +07:00
parent fd08bee7ab
commit 0abf48ea2d
28 changed files with 3514 additions and 37 deletions

View File

@@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react';
import { apiExam, type ExamRoomItem } from '../api';
import { openExam } from './NavHistoryBar';
function fmtTime(iso: string) {
try {
return new Date(iso).toLocaleString('vi-VN');
} catch {
return iso;
}
}
function statusLabel(st: string) {
if (st === 'draft') return { text: 'Tạm thời', cls: 'badge-muted' };
if (st === 'ready') return { text: 'Sẵn sàng', cls: 'badge-info' };
if (st === 'active') return { text: 'Đang thi', cls: 'badge-success' };
if (st === 'cancelled') return { text: 'Đã hủy', cls: 'badge-warning' };
if (st === 'ended') return { text: 'Đã kết thúc', cls: 'badge-muted' };
return { text: st, cls: 'badge-muted' };
}
function toLocalInput(iso?: string) {
if (!iso) return '';
const d = new Date(iso);
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function localInputToISO(v: string) {
if (!v) return '';
return new Date(v).toISOString();
}
export const ExamsTab: React.FC = () => {
const [rooms, setRooms] = useState<ExamRoomItem[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [name, setName] = useState('');
const [start, setStart] = useState('');
const [end, setEnd] = useState('');
const [busy, setBusy] = useState(false);
const load = async () => {
setLoading(true);
try {
const res = await apiExam.list();
setRooms(res.data);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
const create = async () => {
if (!name.trim() || !start || !end) return;
setBusy(true);
try {
const room = await apiExam.create({
name: name.trim(),
startTime: localInputToISO(start),
endTime: localInputToISO(end),
});
setShowCreate(false);
setName('');
openExam(room.id, room.name);
} catch (e: any) {
alert(e?.message || 'Lỗi');
} finally {
setBusy(false);
}
};
return (
<div className="page-stack">
<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>
</div>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>+ Tạo phòng thi</button>
</header>
{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">
<span>Tên phòng thi</span>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="VD: Thi cuối kỳ Java" />
</label>
<label className="login-field">
<span>Bắt đu</span>
<input type="datetime-local" value={start} onChange={(e) => setStart(e.target.value)} />
</label>
<label className="login-field">
<span>Kết thúc</span>
<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>
</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>
</div>
);
};
export { toLocalInput, localInputToISO, fmtTime };