diff --git a/management/src/.env b/management/src/.env new file mode 100644 index 0000000..7770dd8 --- /dev/null +++ b/management/src/.env @@ -0,0 +1 @@ +VITE_API_BASE=https://sv.rikkeiraia.org/api \ No newline at end of file diff --git a/management/src/App.tsx b/management/src/App.tsx index cac98f9..97b46e8 100644 --- a/management/src/App.tsx +++ b/management/src/App.tsx @@ -7,6 +7,7 @@ import { ExamsTab } from './components/ExamsTab'; import { ExamRoomWorkspace } from './components/ExamRoomWorkspace'; import { SystemTab } from './components/SystemTab'; import { MyAccountTab } from './components/AccountsTab'; +import { StudentAffairsTab } from './components/StudentAffairsTab'; import { ChatWidget } from './components/ChatWidget'; import { StaffChatSocket } from './hooks/useStaffChatSocket'; import { ClassWorkspace } from './components/ClassWorkspace'; @@ -68,6 +69,12 @@ const IconSystem = () => ( ); +const IconStudentAffairs = () => ( + + + +); + const IconMenu = () => ( @@ -202,6 +209,17 @@ function App() { +
Công tác sinh viên
+
  • + +
  • +
    Hệ thống
  • @@ -138,8 +144,24 @@ export const ClassesTab: React.FC = () => { {syncStatus.running &&
    } {syncStatus.running && `Đang đồng bộ lớp học... (${syncPercent}%)`} - {syncStatus.done && '🎉 Đồng bộ lớp học hoàn tất!'} - {syncStatus.error && `❌ Đồng bộ thất bại: ${syncStatus.error}`} + {syncStatus.done && ( + + + + + Đồng bộ lớp học hoàn tất! + + )} + {syncStatus.error && ( + + + + + + + Đồng bộ thất bại: {syncStatus.error} + + )}
    @@ -171,7 +193,12 @@ export const ClassesTab: React.FC = () => { setPage(1); }} /> - 🔍 + + + + + +
    @@ -217,7 +244,12 @@ export const ClassesTab: React.FC = () => {
    ) : classes.length === 0 ? (
    -
    🏫
    +
    + + + + +

    Không tìm thấy lớp học nào

    Hãy thử thay đổi bộ lọc hoặc bấm nút "Đồng bộ Lớp học" để kéo dữ liệu mới.

    @@ -249,13 +281,21 @@ export const ClassesTab: React.FC = () => { > {cl.name} -
    - 💼 {cl.specializeName || 'Chưa có chuyên ngành'} +
    + + + + + {cl.specializeName || 'Chưa có chuyên ngành'}
    - - 📍 {cl.systemName || `Hệ thống ${cl.systemRkId || 'Khác'}`} + + + + + + {cl.systemName || `Hệ thống ${cl.systemRkId || 'Khác'}`} @@ -272,8 +312,12 @@ export const ClassesTab: React.FC = () => {
    - - 👤 {cl.studentCount} SV + + + + + + {cl.studentCount} SV @@ -304,7 +348,13 @@ export const ClassesTab: React.FC = () => { style={{ padding: '0.5rem 0.95rem', fontSize: '0.8rem', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '0.35rem' }} onClick={() => openClass('classes', cl.rkId, cl.name)} > - 👥 Xem lớp + + + + + + + Xem lớp @@ -327,8 +377,11 @@ export const ClassesTab: React.FC = () => { className="pagination-btn" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} + style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} > - ◀ + + + Trang {page} / {Math.ceil(total / pageSize) || 1} @@ -337,8 +390,11 @@ export const ClassesTab: React.FC = () => { className="pagination-btn" onClick={() => setPage(p => p + 1)} disabled={page >= Math.ceil(total / pageSize)} + style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} > - ▶ + + + diff --git a/management/src/components/DashboardTab.tsx b/management/src/components/DashboardTab.tsx index bf0b0e5..656e39d 100644 --- a/management/src/components/DashboardTab.tsx +++ b/management/src/components/DashboardTab.tsx @@ -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 = () => ( + + + + +); + +const IconActiveClasses = () => ( + + + +); + +const IconStudents = () => ( + + + + + + +); + +const IconAlert = () => ( + + + + + +); + export const DashboardTab: React.FC = ({ onNavigate }) => { + const { staff } = useAuth(); + const isSuperAdmin = staff?.email === 'phuocntb@rikkeiacademy.com'; + const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + // Guide links state + const [links, setLinks] = useState([]); + const [linksLoading, setLinksLoading] = useState(true); + + // Add/Edit modal state + const [showModal, setShowModal] = useState(false); + const [editingLink, setEditingLink] = useState(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 = ({ 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 (
    @@ -38,86 +142,249 @@ export const DashboardTab: React.FC = ({ onNavigate }) => {
    - {loading && ( -
    -
    -

    Đang tải thông số hệ thống...

    -
    - )} - - {error && ( -
    -
    ⚠️
    -

    Lỗi: {error}

    -
    - )} - - {!loading && !error && stats && ( - <> -
    -
    onNavigate('classes')} style={{ cursor: 'pointer' }}> -
    -
    Tổng lớp học đã đồng bộ
    -
    {stats.totalClasses}
    -
    -
    🏫
    -
    - -
    onNavigate('classes')} style={{ cursor: 'pointer' }}> -
    -
    Lớp đang giảng dạy
    -
    {stats.activeClasses}
    -
    -
    -
    - -
    onNavigate('students')} style={{ cursor: 'pointer' }}> -
    -
    Tổng sinh viên đã đồng bộ
    -
    {stats.totalStudents}
    -
    -
    🎓
    -
    + {loading && ( +
    +
    +

    Đang tải thông số hệ thống...

    + )} -
    -
    -

    Khởi động nhanh

    -

    - Chào mừng bạn đến với trang quản trị Simple Care. - 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ộ. -

    -
    - - + {error && ( +
    +
    + +
    +

    Lỗi: {error}

    +
    + )} + + {!loading && !error && stats && ( + <> +
    +
    onNavigate('classes')} style={{ cursor: 'pointer' }}> +
    +
    Tổng lớp học đã đồng bộ
    +
    {stats.totalClasses}
    +
    +
    + +
    +
    + +
    onNavigate('classes')} style={{ cursor: 'pointer' }}> +
    +
    Lớp đang giảng dạy
    +
    {stats.activeClasses}
    +
    +
    + +
    +
    + +
    onNavigate('students')} style={{ cursor: 'pointer' }}> +
    +
    Tổng sinh viên đã đồng bộ
    +
    {stats.totalStudents}
    +
    +
    + +
    -
    -

    Quy trình hoạt động

    -
      -
    • Đồng bộ lớp học: 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ả.
    • -
    • Quản lý lớp: Lọc theo phân hệ, tìm kiếm tên/mã lớp và bật/tắt cờ Đang học.
    • -
    • Đồng bộ sinh viên: 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ộ.
    • -
    +
    +
    +

    Khởi động nhanh

    +

    + Chào mừng bạn đến với trang quản trị Simple Care. + Hệ thống hỗ trợ quản lý lớp học, thi cử và chăm sóc sinh viên. +

    +
    + + + +
    +
    -
    - - )} + +
    +
    +

    + + + + + + + Tài liệu hướng dẫn +

    + {isSuperAdmin && ( + + )} +
    + + {linksLoading && ( +
    +
    +
    + )} + + {!linksLoading && links.length === 0 && ( +

    Chưa có tài liệu hướng dẫn nào được cấu hình.

    + )} + + {!linksLoading && links.length > 0 && ( +
    + {links.map((link) => ( +
    { + 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'; + }} + > + e.currentTarget.style.color = 'var(--accent)'} + onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-primary)'} + > + + + + + {link.title} + + {isSuperAdmin && ( +
    + + +
    + )} +
    + ))} +
    + )} +
    + + )}
    + + {showModal && ( +
    setShowModal(false)}> +
    e.stopPropagation()}> +
    +

    {editingLink ? 'Cập nhật tài liệu' : 'Thêm tài liệu hướng dẫn'}

    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + )}
    ); }; diff --git a/management/src/components/ExamsTab.tsx b/management/src/components/ExamsTab.tsx index fba07b7..4df98e1 100644 --- a/management/src/components/ExamsTab.tsx +++ b/management/src/components/ExamsTab.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { apiExam, type ExamRoomItem } from '../api'; import { openExam } from './NavHistoryBar'; +import { useAuth } from '../auth/AuthContext'; function fmtTime(iso: string) { try { @@ -32,6 +33,9 @@ function localInputToISO(v: string) { } export const ExamsTab: React.FC = () => { + const { staff } = useAuth(); + const isSuperAdmin = staff?.email === 'phuocntb@rikkeiacademy.com'; + const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); @@ -94,6 +98,18 @@ export const ExamsTab: React.FC = () => { } }; + const handleDeleteRoom = async (roomId: number, roomName: string) => { + if (window.confirm(`Bạn có chắc chắn muốn xóa hoàn toàn phòng thi "${roomName}" không? Hành động này sẽ xóa sạch dữ liệu phòng thi, các bài nộp, và không thể khôi phục.`)) { + try { + await apiExam.remove(roomId); + alert('Đã xóa phòng thi thành công!'); + await load(); + } catch (err: any) { + alert(err.message || 'Xóa phòng thi thất bại'); + } + } + }; + return (
    @@ -190,9 +206,40 @@ export const ExamsTab: React.FC = () => { {r.studentCount} sinh viên · {r.paperCount} đề
    - +
    + + {isSuperAdmin && ( + + )} +
    ); })} diff --git a/management/src/components/LearningTab.tsx b/management/src/components/LearningTab.tsx index 1e87857..e58a916 100644 --- a/management/src/components/LearningTab.tsx +++ b/management/src/components/LearningTab.tsx @@ -266,32 +266,41 @@ export const LearningTab: React.FC = () => {
    {loading && ( -
    - 🔄 -

    Đang tải...

    +
    +
    +

    Đang tải dữ liệu...

    )} {!loading && activeClasses.length === 0 && ( -
    - 📅 -

    Chưa có lớp nào được tạo lịch học.

    -
    )} {!loading && activeClasses.length > 0 && filteredClasses.length === 0 && ( -
    - 🔍 -

    Không tìm thấy lớp phù hợp với bộ lọc hiện tại.

    +
    + + + + +

    Không tìm thấy lớp phù hợp với bộ lọc hiện tại.

    @@ -385,8 +394,8 @@ export const LearningTab: React.FC = () => {
    {configClass && ( -
    -
    +
    setConfigClass(null)}> +
    e.stopPropagation()}>

    Cấu hình lịch học & ứng dụng

    @@ -394,9 +403,9 @@ export const LearningTab: React.FC = () => { {configClass.name} ({configClass.classCode})

    - +
    -
    +

    Ca học trong tuần

    { Từ khóa ngăn chặn các ứng dụng ngoài luồng trên máy sinh viên.