This commit is contained in:
@@ -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 đề và 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>
|
||||
|
||||
Reference in New Issue
Block a user