All checks were successful
Deploy on Master Change / deploy (push) Successful in 40s
147 lines
5.9 KiB
TypeScript
147 lines
5.9 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { api } from '../api';
|
|
import type { StatsResponse } from '../api';
|
|
|
|
interface DashboardTabProps {
|
|
onNavigate: (tab: string) => void;
|
|
}
|
|
|
|
const IconClasses = () => (
|
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M22 10v6M2 10l10-5 10 5-10 5z" />
|
|
<path d="M6 12v5c0 2 2 3 6 3s6-1 6-3v-5" />
|
|
</svg>
|
|
);
|
|
|
|
const IconActiveClasses = () => (
|
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
</svg>
|
|
);
|
|
|
|
const IconStudents = () => (
|
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
</svg>
|
|
);
|
|
|
|
const IconAlert = () => (
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
<line x1="12" y1="9" x2="12" y2="13" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
);
|
|
|
|
export const DashboardTab: React.FC<DashboardTabProps> = ({ onNavigate }) => {
|
|
const [stats, setStats] = useState<StatsResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchStats = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await api.getStats();
|
|
setStats(data);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Không thể tải dữ liệu thống kê');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchStats();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="tab-page">
|
|
<div className="tab-page-toolbar">
|
|
<div className="page-header">
|
|
<div className="page-title">
|
|
<h1>Tổng Quan Hệ Thống</h1>
|
|
<p>Giám sát đồng bộ và quản lý lớp học, sinh viên từ hệ thống chính</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="tab-page-body tab-page-scroll">
|
|
{loading && (
|
|
<div className="empty-state">
|
|
<div className="sync-spinner" style={{ width: '40px', height: '40px', borderWidth: '3px' }}></div>
|
|
<p style={{ marginTop: '1rem' }}>Đang tải thông số hệ thống...</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="empty-state" style={{ border: '1px solid rgba(214, 48, 49, 0.2)', backgroundColor: 'rgba(214, 48, 49, 0.05)', borderRadius: '12px' }}>
|
|
<div className="empty-state-icon" style={{ color: 'var(--danger)' }}>
|
|
<IconAlert />
|
|
</div>
|
|
<p style={{ color: 'var(--danger)', fontWeight: 600 }}>Lỗi: {error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && stats && (
|
|
<>
|
|
<div className="stats-grid">
|
|
<div className="stat-card" onClick={() => onNavigate('classes')} style={{ cursor: 'pointer' }}>
|
|
<div>
|
|
<div className="stat-label">Tổng lớp học đã đồng bộ</div>
|
|
<div className="stat-value">{stats.totalClasses}</div>
|
|
</div>
|
|
<div className="stat-icon" style={{ color: 'var(--accent)' }}>
|
|
<IconClasses />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card" onClick={() => onNavigate('classes')} style={{ cursor: 'pointer' }}>
|
|
<div>
|
|
<div className="stat-label">Lớp đang giảng dạy</div>
|
|
<div className="stat-value" style={{ color: 'var(--success)' }}>{stats.activeClasses}</div>
|
|
</div>
|
|
<div className="stat-icon success" style={{ color: 'var(--success)', backgroundColor: 'var(--success-light)' }}>
|
|
<IconActiveClasses />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card" onClick={() => onNavigate('students')} style={{ cursor: 'pointer' }}>
|
|
<div>
|
|
<div className="stat-label">Tổng sinh viên đã đồng bộ</div>
|
|
<div className="stat-value" style={{ color: 'var(--accent-hover)' }}>{stats.totalStudents}</div>
|
|
</div>
|
|
<div className="stat-icon" style={{ color: 'var(--accent-hover)' }}>
|
|
<IconStudents />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '1.5rem' }}>
|
|
<div className="content-card">
|
|
<h2>Khởi động nhanh</h2>
|
|
<p style={{ lineHeight: '1.6', color: 'var(--text-secondary)' }}>
|
|
Chào mừng bạn đến với trang quản trị <strong>Simple Care</strong>.
|
|
Hệ thống hỗ trợ quản lý lớp học, thi cử và chăm sóc sinh viên.
|
|
</p>
|
|
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginTop: '1rem' }}>
|
|
<button className="btn btn-primary" onClick={() => onNavigate('classes')}>
|
|
Quản lý lớp học
|
|
</button>
|
|
<button className="btn btn-secondary" onClick={() => onNavigate('learning')}>
|
|
Lịch học & Giám sát
|
|
</button>
|
|
<button className="btn btn-secondary" onClick={() => onNavigate('exams')}>
|
|
Quản lý phòng thi
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|