add tai lieu system
All checks were successful
Deploy on Master Change / deploy (push) Successful in 43s

This commit is contained in:
2026-07-01 14:47:46 +07:00
parent 6919513165
commit dd766206f7
15 changed files with 952 additions and 140 deletions

View File

@@ -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 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ớ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 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ớp:</strong> Lọc theo phân hệ, tìm kiếm tên/ lớp 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ớp học, thi cử 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ớ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 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 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">&times;</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>
);
};