add
This commit is contained in:
149
management/src/components/ExamsTab.tsx
Normal file
149
management/src/components/ExamsTab.tsx
Normal 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 đề và 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 có 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 };
|
||||
Reference in New Issue
Block a user