add tai lieu system
All checks were successful
Deploy on Master Change / deploy (push) Successful in 43s
All checks were successful
Deploy on Master Change / deploy (push) Successful in 43s
This commit is contained in:
@@ -1,16 +1,73 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api';
|
||||
import { api, type GuideLinkItem } from '../api';
|
||||
import type { StatsResponse } from '../api';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
|
||||
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 { staff } = useAuth();
|
||||
const isSuperAdmin = staff?.email === 'phuocntb@rikkeiacademy.com';
|
||||
|
||||
const [stats, setStats] = useState<StatsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Guide links state
|
||||
const [links, setLinks] = useState<GuideLinkItem[]>([]);
|
||||
const [linksLoading, setLinksLoading] = useState(true);
|
||||
|
||||
// Add/Edit modal state
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingLink, setEditingLink] = useState<GuideLinkItem | null>(null);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [modalUrl, setModalUrl] = useState('');
|
||||
const [modalSaving, setModalSaving] = useState(false);
|
||||
|
||||
const fetchLinks = async () => {
|
||||
try {
|
||||
setLinksLoading(true);
|
||||
const res = await api.listGuideLinks();
|
||||
setLinks(res.data || []);
|
||||
} catch (err) {
|
||||
console.error('Không thể tải tài liệu hướng dẫn:', err);
|
||||
} finally {
|
||||
setLinksLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
@@ -24,8 +81,55 @@ export const DashboardTab: React.FC<DashboardTabProps> = ({ onNavigate }) => {
|
||||
}
|
||||
};
|
||||
fetchStats();
|
||||
fetchLinks();
|
||||
}, []);
|
||||
|
||||
const handleOpenAddModal = () => {
|
||||
setEditingLink(null);
|
||||
setModalTitle('');
|
||||
setModalUrl('');
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (link: GuideLinkItem) => {
|
||||
setEditingLink(link);
|
||||
setModalTitle(link.title);
|
||||
setModalUrl(link.url);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSaveLink = async () => {
|
||||
if (!modalTitle.trim() || !modalUrl.trim()) {
|
||||
alert('Vui lòng điền đầy đủ tiêu đề và đường dẫn');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setModalSaving(true);
|
||||
if (editingLink) {
|
||||
await api.updateGuideLink(editingLink.id, modalTitle.trim(), modalUrl.trim());
|
||||
} else {
|
||||
await api.createGuideLink(modalTitle.trim(), modalUrl.trim());
|
||||
}
|
||||
setShowModal(false);
|
||||
await fetchLinks();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Lưu tài liệu thất bại');
|
||||
} finally {
|
||||
setModalSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (id: number, title: string) => {
|
||||
if (window.confirm(`Bạn có chắc chắn muốn xóa tài liệu "${title}"?`)) {
|
||||
try {
|
||||
await api.deleteGuideLink(id);
|
||||
await fetchLinks();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Xóa tài liệu thất bại');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tab-page">
|
||||
<div className="tab-page-toolbar">
|
||||
@@ -38,86 +142,249 @@ export const DashboardTab: React.FC<DashboardTabProps> = ({ onNavigate }) => {
|
||||
</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(239, 68, 68, 0.2)', backgroundColor: 'rgba(239, 68, 68, 0.05)' }}>
|
||||
<div className="empty-state-icon" style={{ color: 'var(--danger)' }}>⚠️</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">🏫</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">✓</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">🎓</div>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<div className="content-grid-2">
|
||||
<div className="content-card">
|
||||
<h2>Khởi động nhanh</h2>
|
||||
<p>
|
||||
Chào mừng bạn đến với trang quản trị <strong>Simple Care</strong>.
|
||||
Hệ thống hỗ trợ đồng bộ hóa thông tin tự động từ cổng đào tạo chính (QLĐT), giúp lưu trữ, cập nhật trạng thái lớp học và danh sách sinh viên nội bộ.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary" onClick={() => onNavigate('classes')}>
|
||||
Quản lý lớp học
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => onNavigate('students')}>
|
||||
Danh sách sinh viên
|
||||
</button>
|
||||
{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 className="content-card">
|
||||
<h2>Quy trình hoạt động</h2>
|
||||
<ul style={{
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.75,
|
||||
paddingLeft: '1.15rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
margin: 0
|
||||
}}>
|
||||
<li><strong>Đồng bộ lớp học:</strong> Tải danh sách lớp và môn học, kéo thông tin sinh viên từng lớp qua bảng kết quả.</li>
|
||||
<li><strong>Quản lý lớp:</strong> Lọc theo phân hệ, tìm kiếm tên/mã lớp và bật/tắt cờ <strong>Đang học</strong>.</li>
|
||||
<li><strong>Đồng bộ sinh viên:</strong> Kéo toàn bộ sinh viên từ hệ thống chính (kèm thông tin liên hệ, hệ học) vào database nội bộ.</li>
|
||||
</ul>
|
||||
<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 className="content-card" style={{ marginTop: '1.5rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', borderBottom: '1px solid var(--border-color)', paddingBottom: '0.75rem' }}>
|
||||
<h2 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--accent)' }}>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
Tài liệu hướng dẫn
|
||||
</h2>
|
||||
{isSuperAdmin && (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleOpenAddModal}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
||||
>
|
||||
+ Thêm tài liệu
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{linksLoading && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '1rem' }}>
|
||||
<div className="sync-spinner" style={{ width: '24px', height: '24px' }}></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!linksLoading && links.length === 0 && (
|
||||
<p style={{ color: 'var(--text-muted)', fontStyle: 'italic', fontSize: '0.9rem', margin: '0.5rem 0' }}>Chưa có tài liệu hướng dẫn nào được cấu hình.</p>
|
||||
)}
|
||||
|
||||
{!linksLoading && links.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{links.map((link) => (
|
||||
<div
|
||||
key={link.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '0.75rem 1rem',
|
||||
backgroundColor: 'var(--bg-app)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-color)',
|
||||
transition: 'var(--transition)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(187, 33, 38, 0.25)';
|
||||
e.currentTarget.style.backgroundColor = '#fff';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-sm)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--border-color)';
|
||||
e.currentTarget.style.backgroundColor = 'var(--bg-app)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
color: 'var(--text-primary)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.92rem',
|
||||
flex: 1
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--accent)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: '#4285F4' }}>
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
{link.title}
|
||||
</a>
|
||||
{isSuperAdmin && (
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleOpenEditModal(link)}
|
||||
style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem' }}
|
||||
>
|
||||
Sửa
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleDeleteLink(link.id, link.title)}
|
||||
style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', color: 'var(--danger)', borderColor: 'rgba(214, 48, 49, 0.2)' }}
|
||||
>
|
||||
Xóa
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
||||
<div className="modal-container" style={{ maxWidth: '450px' }} onClick={e => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">{editingLink ? 'Cập nhật tài liệu' : 'Thêm tài liệu hướng dẫn'}</h2>
|
||||
<button className="modal-close-btn" onClick={() => setShowModal(false)} aria-label="Đóng">×</button>
|
||||
</div>
|
||||
<div className="modal-body" style={{ padding: '1.5rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Tiêu đề tài liệu</span>
|
||||
<input
|
||||
type="text"
|
||||
value={modalTitle}
|
||||
onChange={e => setModalTitle(e.target.value)}
|
||||
placeholder="VD: Hướng dẫn coi thi cuối kỳ"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.55rem 0.75rem',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-color)',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'var(--transition)'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
||||
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
||||
/>
|
||||
</label>
|
||||
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Đường dẫn Google Docs</span>
|
||||
<input
|
||||
type="url"
|
||||
value={modalUrl}
|
||||
onChange={e => setModalUrl(e.target.value)}
|
||||
placeholder="https://docs.google.com/document/d/..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.55rem 0.75rem',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-color)',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'var(--transition)'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
||||
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer" style={{ padding: '1rem 1.5rem' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Hủy</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSaveLink}
|
||||
disabled={modalSaving}
|
||||
>
|
||||
{modalSaving ? 'Đang lưu...' : 'Lưu lại'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user