diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 6357a42..2843c06 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -952,8 +952,38 @@ router.get('/admin/users', protect, async (req, res) => { return res.status(403).json({ message: 'Bạn không có quyền truy cập quản trị' }); } try { - const users = await User.find({}).sort({ createdAt: -1 }); - res.json(users); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const totalUsers = await User.countDocuments(); + + // Sử dụng Aggregation để sắp xếp Role ưu tiên và Phân trang + const users = await User.aggregate([ + { + $addFields: { + roleOrder: { + $switch: { + branches: [ + { case: { $eq: ["$role", "admin"] }, then: 0 }, + { case: { $eq: ["$role", "Chủ sở hữu"] }, then: 1 } + ], + default: 2 + } + } + } + }, + { $sort: { roleOrder: 1, createdAt: -1 } }, + { $skip: skip }, + { $limit: limit } + ]); + + res.json({ + users, + totalPages: Math.ceil(totalUsers / limit), + currentPage: page, + totalUsers + }); } catch (error) { res.status(500).json({ message: error.message }); } @@ -970,14 +1000,20 @@ router.put('/admin/users/:id', protect, async (req, res) => { const user = await User.findById(req.params.id); if (!user) return res.status(404).json({ message: 'Người dùng không tồn tại' }); - // Cập nhật thông tin cơ bản - if (fullName) user.fullName = fullName; - if (email) user.email = email; - if (role) user.role = role; - - // Nếu có nhập mật khẩu mới thì cập nhật (Middleware pre-save sẽ tự hash) - if (password && password.trim() !== '') { - user.password = password; + if (user.role === 'admin') { + // Theo yêu cầu: Admin tối cao chỉ được sửa Họ tên và Email + if (fullName) user.fullName = fullName; + if (email) user.email = email; + // Bỏ qua role và password để bảo vệ tài khoản root + } else { + // User bình thường được sửa tất cả + if (fullName) user.fullName = fullName; + if (email) user.email = email; + if (role) user.role = role; + + if (password && password.trim() !== '') { + user.password = password; + } } await user.save(); @@ -997,8 +1033,8 @@ router.delete('/admin/users/:id', protect, async (req, res) => { const user = await User.findById(req.params.id); if (!user) return res.status(404).json({ message: 'Người dùng không tồn tại' }); - if (user.role === 'admin' && user._id.toString() === req.user._id.toString()) { - return res.status(400).json({ message: 'Bạn không thể tự xóa chính mình' }); + if (user.role === 'admin') { + return res.status(400).json({ message: 'Không thể xóa tài khoản Admin tối cao' }); } // Lưu ý: Trong thực tế bạn có thể muốn xóa cả các Scene của user này diff --git a/frontend/css/style.css b/frontend/css/style.css index c35097c..37be8f3 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -992,6 +992,7 @@ html, body { padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.1); + vertical-align: middle; } .admin-table th { @@ -999,6 +1000,32 @@ html, body { font-weight: 600; text-transform: uppercase; font-size: 12px; + white-space: nowrap; +} + +/* Thiết lập độ rộng tối thiểu để tránh co rúm nội dung */ +.admin-table th:nth-child(1) { min-width: 160px; } /* Họ tên */ +.admin-table th:nth-child(2) { min-width: 120px; } /* Username */ +.admin-table th:nth-child(3) { min-width: 200px; } /* Email */ +.admin-table th:nth-child(4) { min-width: 130px; } /* Quyền hạn */ +.admin-table th:nth-child(5) { min-width: 140px; } /* Reset Password */ +.admin-table th:nth-child(6) { min-width: 140px; } /* Thao tác */ + +.admin-table td input, .admin-table td select { + background: #2a2a2a !important; + border: 1px solid #444 !important; + color: #fff !important; + padding: 8px !important; + border-radius: 4px; + width: 100%; + font-size: 13px; +} + +/* Fix lỗi dropdown trắng trên nền trắng */ +.admin-table select option { + background-color: #1e1e1e; + color: #ffffff; + padding: 10px; } .admin-table input, .admin-table select { @@ -1010,6 +1037,43 @@ html, body { width: 100%; } +/* Pagination Styles */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-top: 25px; + padding-top: 15px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.pagination-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + transition: all 0.2s; +} + +.pagination-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.2); + border-color: #007bff; +} + +.pagination-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.pagination-info { + color: #888; + font-size: 13px; +} + /* --- Privacy Settings Enhancements --- */ .privacy-settings-btn { background: rgba(255, 255, 255, 0.1); diff --git a/frontend/index.html b/frontend/index.html index 3cb20b5..f11879d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -84,7 +84,7 @@ - + @@ -119,8 +119,9 @@
Đang tải danh sách người dùng...
'; + if (paginationContainer) paginationContainer.innerHTML = ''; try { - const res = await fetch(`${API_BASE_URL}/admin/users`, { + const res = await fetch(`${API_BASE_URL}/admin/users?page=${page}&limit=10`, { headers: { 'Authorization': `Bearer ${token}` } }); - const users = await res.json(); - if (!res.ok) throw new Error(users.message); + const data = await res.json(); + if (!res.ok) throw new Error(data.message); + + const { users, totalPages, currentPage, totalUsers } = data; let html = `| ${user.username} | - | -+ | - + ${isRootAdmin ? '' : ``} |
Lỗi: ${e.message}
`; }