"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; import type { User, UserRole, UserStatus } from "@/types"; // ── Helpers ─────────────────────────────────────────────────────────────────── const STATUS_LABELS: Record = { pending: "Chờ duyệt", approved: "Đã duyệt", rejected: "Từ chối", }; const ROLE_LABELS: Record = { admin: "Admin", teacher: "Giáo viên", student: "Học viên", }; const STATUS_COLORS: Record = { pending: "bg-yellow-100 text-yellow-800", approved: "bg-green-100 text-green-800", rejected: "bg-red-100 text-red-800", }; const ROLE_COLORS: Record = { admin: "bg-purple-100 text-purple-800", teacher: "bg-blue-100 text-blue-800", student: "bg-gray-100 text-gray-700", }; // ── Page ────────────────────────────────────────────────────────────────────── export default function AdminPage() { const router = useRouter(); const [me, setMe] = useState(null); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [busy, setBusy] = useState(null); // userId being mutated // Backup / Restore state const [backupLoading, setBackupLoading] = useState(false); const [restoreLoading, setRestoreLoading] = useState(false); const [backupMsg, setBackupMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null); const restoreInputRef = useState(null); const [filterStatus, setFilterStatus] = useState("all"); const [filterRole, setFilterRole] = useState("all"); const [search, setSearch] = useState(""); // ── Auth guard: admin only ───────────────────────────────────────────────── useEffect(() => { async function init() { try { const me = await api.me(); if (me.role !== "admin") { router.replace("/dashboard"); return; } setMe(me); const list = await api.adminListUsers(); setUsers(list); } catch { router.replace("/login"); } finally { setLoading(false); } } init(); }, [router]); // ── Mutate helpers ───────────────────────────────────────────────────────── const updateUser = useCallback((updated: User) => { setUsers(prev => prev.map(u => u.id === updated.id ? updated : u)); }, []); const handleStatusChange = useCallback(async (userId: number, status: UserStatus) => { setBusy(userId); try { const updated = await api.adminUpdateStatus(userId, status); updateUser(updated); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to update status."); } finally { setBusy(null); } }, [updateUser]); const handleRoleChange = useCallback(async (userId: number, role: UserRole) => { setBusy(userId); try { const updated = await api.adminUpdateRole(userId, role); updateUser(updated); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to update role."); } finally { setBusy(null); } }, [updateUser]); const handleDelete = useCallback(async (userId: number, username: string) => { if (!confirm(`Xoá tài khoản "${username}" và toàn bộ dữ liệu? Không thể hoàn tác.`)) return; setBusy(userId); try { await api.adminDeleteUser(userId); setUsers(prev => prev.filter(u => u.id !== userId)); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to delete user."); } finally { setBusy(null); } }, []); // ── Filtered list ────────────────────────────────────────────────────────── const filtered = users.filter(u => { if (filterStatus !== "all" && u.status !== filterStatus) return false; if (filterRole !== "all" && u.role !== filterRole) return false; if (search) { const q = search.toLowerCase(); if (!u.username.toLowerCase().includes(q) && !u.email.toLowerCase().includes(q)) return false; } return true; }); const pendingCount = users.filter(u => u.status === "pending").length; // ── Loading ──────────────────────────────────────────────────────────────── if (loading) { return (
); } return (
{/* ── Navbar ──────────────────────────────────────────────────────────── */}
PDF LMS Admin
{me?.username}
{/* ── Page title + stats ─────────────────────────────────────────────── */}

Quản lý người dùng

{users.length} tài khoản tổng cộng

{pendingCount > 0 && (
setFilterStatus("pending")} > {pendingCount} tài khoản chờ duyệt
)}
{/* ── Error banner ──────────────────────────────────────────────────── */} {error && (
{error}
)} {/* ── Filters ───────────────────────────────────────────────────────── */}
setSearch(e.target.value)} className="flex-1 text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" /> {(filterStatus !== "all" || filterRole !== "all" || search) && ( )}
{/* ── Backup & Restore ─────────────────────────────────────────────── */}

Sao lưu & Khôi phục

File ZIP chứa toàn bộ cơ sở dữ liệu (SQL dump) và các file PDF đã tải lên. Dùng để di chuyển sang server khác.

{backupMsg && (
{backupMsg.text}
)}
{/* Download backup */} {/* Restore */}

⚠️ Khôi phục sẽ ghi đè toàn bộ dữ liệu hiện tại (người dùng, PDF, annotation). Hãy tải backup trước khi khôi phục.

{/* ── Table ────────────────────────────────────────────────────────── */}
{filtered.length === 0 ? (
Không tìm thấy tài khoản nào.
) : (
{filtered.map(u => { const isSelf = u.id === me?.id; const isLoading = busy === u.id; return ( {/* ID */} {/* User info */} {/* Role selector */} {/* Status selector */} {/* Created at */} {/* Actions */} ); })}
ID Người dùng Vai trò Trạng thái Ngày đăng ký Hành động
{u.id}
{u.username}
{u.email}
{isSelf ? ( {ROLE_LABELS[u.role]} ) : ( )} {isSelf ? ( {STATUS_LABELS[u.status]} ) : ( )} {new Date(u.created_at).toLocaleDateString("vi-VN")} {isLoading ? ( ) : isSelf ? ( ) : ( )}
)}
); }