Spaces:
Sleeping
Sleeping
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Quản lý người dùng</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='style_admin.css') }}"> | |
| </head> | |
| <body> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header">Quản lý hệ thống</div> | |
| <hr> | |
| <nav class="sidebar-nav"> | |
| <ul> | |
| <li><a href="#" id="dashboardLink"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li> | |
| <li><a href="#" id="usersLink" class="active"><i class="fas fa-users"></i> Người dùng</a></li> | |
| <li><a href="#" id="pendingUsersLink"><i class="fas fa-user-clock"></i> Người dùng chờ xác thực</a></li> | |
| <li><a href="#"><i class="fas fa-cog"></i> Cài đặt</a></li> | |
| <li><a href="#"><i class="fas fa-chart-bar"></i> Báo cáo</a></li> | |
| <li><a href="#"><i class="fas fa-user-circle"></i> Hồ sơ</a></li> | |
| <li><a href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Đăng xuất</a></li> | |
| </ul> | |
| </nav> | |
| </div> | |
| <div class="main-content" id="mainContent"> | |
| <div class="top-bar"> | |
| <button class="menu-toggle" id="menuToggle"><i class="fas fa-bars"></i></button> | |
| <span>Xin chào, {{ user_name | default('Admin') | e }}!</span> | |
| <button onclick="logout()">Đăng xuất</button> | |
| </div> | |
| <div class="content-area" id="contentArea"> | |
| <!-- Users Tab --> | |
| <div id="usersTab"> | |
| <div class="content-header"> | |
| <h1>Danh sách người dùng</h1> | |
| <button class="add-new-btn"><i class="fas fa-plus"></i> Thêm mới</button> | |
| </div> | |
| <div class="search-bar"> | |
| <i class="fas fa-search"></i> | |
| <input type="text" placeholder="Tìm kiếm người dùng..."> | |
| </div> | |
| <div class="table-responsive"> | |
| <table class="table table-striped table-bordered" id="userTable"> | |
| <thead class="table-success"> | |
| <tr> | |
| <th>ID</th> | |
| <th>Tên người dùng</th> | |
| <th>Vai trò</th> | |
| <th>Email</th> | |
| <th>Số điện thoại</th> | |
| <th>Loại tài khoản</th> | |
| <th>Lượt hỏi đáp</th> | |
| <th>Hành động</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for user in users %} | |
| <tr> | |
| <td data-label="ID">{{ user.id }}</td> | |
| <td data-label="Tên người dùng">{{ user.username | e }}</td> | |
| <td data-label="Vai trò">{% if user.is_admin %}Admin{% else %}User{% endif %}</td> | |
| <td data-label="Email">{{ user.email | e }}</td> | |
| <td data-label="Số điện thoại">{{ user.phone | e }}</td> | |
| <td data-label="Loại tài khoản"> | |
| {% if user.account_type == 'limited' %} | |
| <span class="badge badge-pill badge-warning">Giới hạn</span> | |
| {% else %} | |
| <span class="badge badge-pill badge-success">Không giới hạn</span> | |
| {% endif %} | |
| </td> | |
| <td data-label="Lượt hỏi đáp"> | |
| {% if user.account_type == 'unlimited' %} | |
| <span class="query-normal">Không giới hạn</span> | |
| {% elif user.query_limit %} | |
| <span class="query-normal">{{ user.query_count }}/{{ user.query_limit | e }}</span> | |
| {% else %} | |
| <span class="query-normal">{{ user.query_count }}/{{ user.account_type | e }}</span> | |
| {% endif %} | |
| </td> | |
| <td data-label="Hành động" class="action-buttons"> | |
| <button class="edit-btn update-btn" onclick="openUpdateModal('{{ user.id | e }}', '{{ user.account_type | e }}', {{ 'true' if user.is_admin else 'false' }}, '{{ user.query_limit | default('') | e }}')"> | |
| <i class="fas fa-pencil-alt"></i> | |
| </button> | |
| <button class="delete-btn" onclick="deleteUser('{{ user.id | e }}')"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| <button class="reset-btn" onclick="resetQuery('{{ user.id | e }}')"> | |
| <i class="fas fa-redo-alt"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Pending Users Tab --> | |
| <div id="pendingUsersTab" style="display: none;"> | |
| <div class="content-header"> | |
| <h1>Danh sách người dùng chờ xác thực</h1> | |
| </div> | |
| <div class="table-responsive"> | |
| <table class="table table-striped table-bordered" id="pendingUserTable"> | |
| <thead class="table-success"> | |
| <tr> | |
| <th>ID</th> | |
| <th>Tên người dùng</th> | |
| <th>Email</th> | |
| <th>Số điện thoại</th> | |
| <th>Loại tài khoản</th> | |
| <th>Ngày đăng ký</th> | |
| <th>Hành động</th> | |
| </tr> | |
| </thead> | |
| <tbody id="pendingUsersBody"> | |
| <!-- Populated by JavaScript --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Update Modal (Existing) --> | |
| <div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title" id="updateModalLabel">Cập nhật người dùng</h5> | |
| <button type="button" class="btn-close" onclick="closeUpdateModal()" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="updateForm"> | |
| <input type="hidden" id="updateUserId"> | |
| <div class="mb-3"> | |
| <label for="updateAccountType" class="form-label">Loại tài khoản:</label> | |
| <select id="updateAccountType" class="form-select" onchange="toggleQueryLimit()"> | |
| <option value="limited">Limited</option> | |
| <option value="unlimited">Unlimited</option> | |
| </select> | |
| </div> | |
| <div class="mb-3 form-check"> | |
| <input type="checkbox" class="form-check-input" id="updateIsAdmin"> | |
| <label class="form-check-label" for="updateIsAdmin">Admin</label> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="updateQueryLimit class="form-label">Giới hạn hỏi đáp (cho tài khoản limited):</label> | |
| <input type="number" class="form-control" id="updateQueryLimit" min="1" placeholder="Nhập giới hạn (ví dụ: 10)"> | |
| </div> | |
| <div class="d-flex justify-content-end"> | |
| <button type="submit" class="btn btn-success me-2">Lưu</button> | |
| <button type="button" class="btn btn-secondary cancel-btn" onclick="closeUpdateModal()">Hủy</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- jQuery and Bootstrap JS --> | |
| <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
| <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script> | |
| <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script> | |
| <script> | |
| // Sidebar toggle | |
| document.getElementById('menuToggle').addEventListener('click', () => { | |
| const sidebar = document.getElementById('sidebar'); | |
| const mainContent = document.getElementById('mainContent'); | |
| sidebar.classList.toggle('collapsed'); | |
| mainContent.classList.toggle('shifted'); | |
| }); | |
| // Tab switching | |
| document.getElementById('usersLink').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| document.getElementById('usersTab').style.display = 'block'; | |
| document.getElementById('pendingUsersTab').style.display = 'none'; | |
| document.getElementById('usersLink').classList.add('active'); | |
| document.getElementById('pendingUsersLink').classList.remove('active'); | |
| }); | |
| document.getElementById('pendingUsersLink').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| document.getElementById('usersTab').style.display = 'none'; | |
| document.getElementById('pendingUsersTab').style.display = 'block'; | |
| document.getElementById('usersLink').classList.remove('active'); | |
| document.getElementById('pendingUsersLink').classList.add('active'); | |
| loadPendingUsers(); | |
| }); | |
| // Initialize DataTables for users | |
| $(document).ready(function () { | |
| $('#userTable').DataTable({ | |
| language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/vi.json' }, | |
| pageLength: 10, | |
| responsive: true, | |
| columnDefs: [{ orderable: false, targets: 6 }] | |
| }); | |
| $('#pendingUserTable').DataTable({ | |
| language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/vi.json' }, | |
| pageLength: 10, | |
| responsive: true, | |
| columnDefs: [{ orderable: false, targets: 6 }], | |
| searching: false | |
| }); | |
| }); | |
| // Load pending users | |
| async function loadPendingUsers() { | |
| try { | |
| const response = await fetch('/admin/pending_users', { | |
| method: 'GET', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const users = await response.json(); | |
| const tbody = document.getElementById('pendingUsersBody'); | |
| tbody.innerHTML = ''; | |
| users.forEach(user => { | |
| const row = ` | |
| <tr> | |
| <td data-label="ID">${user.id}</td> | |
| <td data-label="Tên người dùng">${user.username}</td> | |
| <td data-label="Email">${user.email}</td> | |
| <td data-label="Số điện thoại">${user.phone}</td> | |
| <td data-label="Loại tài khoản"> | |
| <span class="badge badge-pill ${user.account_type === 'limited' ? 'badge-warning' : 'badge-success'}"> | |
| ${user.account_type === 'limited' ? 'Giới hạn' : 'Không giới hạn'} | |
| </span> | |
| </td> | |
| <td data-label="Ngày đăng ký">${user.created_at}</td> | |
| <td data-label="Hành động" class="action-buttons"> | |
| <button class="btn btn-success btn-sm bg-success" onclick="verifyUser('${user.id}', 'approve')"> | |
| <i class="fas fa-check"></i> | |
| </button> | |
| <button class="btn btn-danger btn-sm bg-danger" onclick="verifyUser('${user.id}', 'reject')"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </td> | |
| </tr>`; | |
| tbody.insertAdjacentHTML('beforeend', row); | |
| }); | |
| } catch (error) { | |
| alert('Lỗi khi tải danh sách người dùng chờ xác thực: ' + error.message); | |
| } | |
| } | |
| // Verify user (approve/reject) | |
| async function verifyUser(userId, action) { | |
| if (!confirm(`Bạn có chắc muốn ${action === 'approve' ? 'chấp nhận' : 'từ chối'} người dùng này?`)) return; | |
| try { | |
| const response = await fetch(`/admin/user/${userId}/verify`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action }) | |
| }); | |
| const result = await response.json(); | |
| alert(result.message || result.error); | |
| loadPendingUsers(); // Refresh pending users list | |
| } catch (error) { | |
| alert('Lỗi khi xử lý: ' + error.message); | |
| } | |
| } | |
| // Existing functions (deleteUser, resetQuery, openUpdateModal, etc.) | |
| async function deleteUser(userId) { | |
| if (confirm('Bạn có chắc muốn xóa người dùng này?')) { | |
| const response = await fetch(`/admin/user/${userId}`, { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const result = await response.json(); | |
| alert(result.message || result.error); | |
| location.reload(); | |
| } | |
| } | |
| async function resetQuery(userId) { | |
| const response = await fetch(`/admin/user/${userId}/reset_query`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const result = await response.json(); | |
| alert(result.message || result.error); | |
| location.reload(); | |
| } | |
| function openUpdateModal(userId, accountType, isAdmin, queryLimit) { | |
| document.getElementById('updateUserId').value = userId; | |
| document.getElementById('updateAccountType').value = accountType; | |
| document.getElementById('updateIsAdmin').checked = isAdmin; | |
| document.getElementById('updateQueryLimit').value = queryLimit || ''; | |
| toggleQueryLimit(); | |
| const modal = new bootstrap.Modal(document.getElementById('updateModal')); | |
| modal.show(); | |
| } | |
| function closeUpdateModal() { | |
| const modal = bootstrap.Modal.getInstance(document.getElementById('updateModal')); | |
| modal.hide(); | |
| } | |
| function toggleQueryLimit() { | |
| const accountType = document.getElementById('updateAccountType').value; | |
| const queryLimitInput = document.getElementById('updateQueryLimit'); | |
| queryLimitInput.disabled = accountType === 'unlimited'; | |
| if (accountType === 'unlimited') { | |
| queryLimitInput.value = ''; | |
| } | |
| } | |
| async function logout() { | |
| try { | |
| const response = await fetch('/logout', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| if (response.ok) { | |
| alert('Đăng xuất thành công'); | |
| window.location.href = '/'; | |
| } else { | |
| alert('Lỗi khi đăng xuất'); | |
| } | |
| } catch (error) { | |
| alert('Lỗi khi đăng xuất: ' + error.message); | |
| } | |
| } | |
| document.getElementById('updateForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const userId = document.getElementById('updateUserId').value; | |
| const accountType = document.getElementById('updateAccountType').value; | |
| const isAdmin = document.getElementById('updateIsAdmin').checked; | |
| const queryLimit = document.getElementById('updateQueryLimit').value; | |
| if (accountType === 'limited' && queryLimit && !/^\d+$/.test(queryLimit)) { | |
| alert('Giới hạn hỏi đáp phải là một số nguyên dương.'); | |
| return; | |
| } | |
| const updates = { account_type: accountType, is_admin: isAdmin }; | |
| if (accountType === 'limited' && queryLimit) { | |
| updates.query_limit = parseInt(queryLimit); | |
| } | |
| try { | |
| const response = await fetch(`/admin/user/${userId}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(updates) | |
| }); | |
| const result = await response.json(); | |
| alert(result.message || result.error); | |
| closeUpdateModal(); | |
| location.reload(); | |
| } catch (error) { | |
| alert('Lỗi khi cập nhật người dùng: ' + error.message); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |