Spaces:
Sleeping
Sleeping
| /** | |
| * Admin Panel - User Management Logic (Premium Edition) | |
| */ | |
| let allMgmtUsers = []; | |
| let filteredMgmtUsers = []; | |
| let deleteTargetId = null; | |
| let activeExpandedId = null; | |
| async function loadUserManagement() { | |
| const grid = document.getElementById('users-mgmt-grid'); | |
| if (!grid) return; | |
| grid.innerHTML = '<div class="loading-state" style="padding:60px;"><i class="fa-solid fa-spinner fa-spin"></i><span>Fetching user directory...</span></div>'; | |
| try { | |
| const res = await fetch('/api/admin/users'); | |
| allMgmtUsers = await res.json(); | |
| updateUserMgmtStats(allMgmtUsers); | |
| applyUserMgmtFilters(); | |
| } catch (err) { | |
| grid.innerHTML = '<div class="error-state">Error loading user management data.</div>'; | |
| } | |
| } | |
| function updateUserMgmtStats(users) { | |
| const totalEl = document.getElementById('total-users-val'); | |
| const activeEl = document.getElementById('active-users-val'); | |
| const disabledEl = document.getElementById('disabled-users-val'); | |
| if (totalEl) totalEl.innerText = users.length; | |
| if (activeEl) activeEl.innerText = users.filter(u => u.is_active).length; | |
| if (disabledEl) disabledEl.innerText = users.filter(u => !u.is_active).length; | |
| } | |
| function applyUserMgmtFilters() { | |
| const searchInput = document.getElementById('user-mgmt-search'); | |
| const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : ''; | |
| // We'll also use a global 'currentMgmtFilter' set by the tabs | |
| const activeTab = document.querySelector('.u-tab-m.active'); | |
| const filterRole = activeTab ? activeTab.getAttribute('onclick').match(/'([^']+)'/)[1] : 'all'; | |
| filteredMgmtUsers = allMgmtUsers.filter(u => { | |
| const searchable = `${u.id} ${u.name} ${u.phone} ${u.email}`.toLowerCase(); | |
| const matchesSearch = searchTerm === '' || searchable.includes(searchTerm); | |
| const matchesRole = filterRole === 'all' || u.role === filterRole; | |
| return matchesSearch && matchesRole; | |
| }); | |
| renderUserMgmtGrid(filteredMgmtUsers); | |
| } | |
| function filterUserMgmt(role, btn) { | |
| document.querySelectorAll('.u-tab-m').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| applyUserMgmtFilters(); | |
| } | |
| function renderUserMgmtGrid(users) { | |
| const grid = document.getElementById('users-mgmt-grid'); | |
| if (!grid) return; | |
| if (users.length === 0) { | |
| grid.innerHTML = '<div class="empty-state" style="padding:60px; grid-column: 1/-1;"><i class="fa-solid fa-user-slash"></i><span>No matching users found.</span></div>'; | |
| return; | |
| } | |
| grid.innerHTML = users.map(u => { | |
| const initial = (u.name || 'U').charAt(0).toUpperCase(); | |
| const status = u.is_active ? 'active' : 'disabled'; | |
| const isExpanded = activeExpandedId === u.id; | |
| // Role-Based Styling | |
| let crystalClass = 'crystal-user'; | |
| let roleLabel = 'General User'; | |
| let roleIcon = 'fa-user-shield'; | |
| if (u.role === 'admin') { | |
| crystalClass = 'crystal-admin'; | |
| roleLabel = 'System Admin'; | |
| roleIcon = 'fa-lock'; | |
| } else if (u.role === 'admin-user') { | |
| crystalClass = 'crystal-agent'; | |
| roleLabel = 'Agent / Moderator'; | |
| roleIcon = 'fa-user-tie'; | |
| } | |
| return ` | |
| <div class="card-u-standard ${!u.is_active ? 'is-disabled' : ''} ${isExpanded ? 'expanded' : ''}" id="user-m-card-${u.id}"> | |
| <!-- Header --> | |
| <div class="card-u-header"> | |
| <div class="card-u-avatar ${crystalClass}">${initial}</div> | |
| <div class="card-u-info"> | |
| <div class="card-u-name">${escapeHtml(u.name)}</div> | |
| <div class="card-u-phone"><i class="fa-solid fa-phone"></i> ${escapeHtml(u.phone)}</div> | |
| </div> | |
| <div class="status-pill-u ${status}">${status}</div> | |
| </div> | |
| <!-- Body Swap Zone --> | |
| <div class="card-u-body-wrap"> | |
| <div class="card-u-meta animate-scale" id="user-meta-${u.id}"> | |
| <span><i class="fa-solid fa-fingerprint"></i> ${u.id.slice(0, 10)}...</span> | |
| <span><i class="fa-solid fa-clock-rotate-left"></i> ${escapeHtml(u.joined_at.split(' ')[0])}</span> | |
| </div> | |
| <div class="card-u-expansion animate-scale" id="user-expansion-${u.id}" style="display: none;"></div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="card-u-actions" id="user-actions-${u.id}"> | |
| <button class="btn-u-action" id="user-manage-btn-${u.id}" onclick="toggleUserCardExpansion('${u.id}')"> | |
| <i class="fa-solid fa-gears"></i> Manage Access | |
| </button> | |
| ${u.role !== 'admin' ? ` | |
| <button class="btn-u-action ${u.is_active ? 'warning' : 'success'}" onclick="toggleUserMgmtStatus('${u.id}')"> | |
| <i class="fa-solid ${u.is_active ? 'fa-ban' : 'fa-check'}"></i> | |
| ${u.is_active ? 'Disable' : 'Enable'} | |
| </button> | |
| ` : ` | |
| <button class="btn-u-action disabled" disabled title="Admin users cannot be disabled"> | |
| <i class="fa-solid fa-shield-halved"></i> Protected | |
| </button> | |
| `} | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function toggleUserCardExpansion(userId) { | |
| console.log('toggleUserCardExpansion called for:', userId); | |
| const card = document.getElementById(`user-m-card-${userId}`); | |
| const metaDiv = document.getElementById(`user-meta-${userId}`); | |
| const expansionDiv = document.getElementById(`user-expansion-${userId}`); | |
| const manageBtn = document.getElementById(`user-manage-btn-${userId}`); | |
| const actionsDiv = document.getElementById(`user-actions-${userId}`); | |
| console.log('Elements found:', { card: !!card, metaDiv: !!metaDiv, expansionDiv: !!expansionDiv, manageBtn: !!manageBtn, actionsDiv: !!actionsDiv }); | |
| if (!card || !metaDiv || !expansionDiv) { | |
| console.error('Required elements not found!'); | |
| return; | |
| } | |
| const isExpanded = card.classList.contains('expanded'); | |
| console.log('Is expanded:', isExpanded); | |
| if (!isExpanded) { | |
| // Expand this card | |
| console.log('Expanding card...'); | |
| card.classList.add('expanded'); | |
| metaDiv.style.display = 'none'; | |
| expansionDiv.style.display = 'flex'; | |
| // Find user data | |
| const user = allMgmtUsers.find(u => u.id === userId) || filteredMgmtUsers.find(u => u.id === userId); | |
| console.log('User data:', user); | |
| if (!user) return; | |
| // Build expansion content | |
| const isAdmin = user.role === 'admin'; | |
| expansionDiv.innerHTML = ` | |
| <div class="u-tool-section"> | |
| <h5>Security Override</h5> | |
| <div class="u-input-row"> | |
| <input type="password" id="new-pass-${userId}" class="u-input-premium" placeholder="Set New Password"> | |
| </div> | |
| </div> | |
| <div class="u-tool-section"> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="btn-u-action primary" onclick="openChangePasswordModal('${userId}')" style="flex: 1;"> | |
| <i class="fa-solid fa-key"></i> Change Password | |
| </button> | |
| ${!isAdmin ? ` | |
| <button class="btn-u-action danger" onclick="openDeleteUserModal('${userId}', '${escapeHtml(user.name)}')" style="flex: 1;"> | |
| <i class="fa-solid fa-trash-can"></i> Delete User | |
| </button> | |
| ` : ` | |
| <button class="btn-u-action disabled" disabled title="Admin users cannot be deleted" style="flex: 1;"> | |
| <i class="fa-solid fa-shield-halved"></i> Protected | |
| </button> | |
| `} | |
| </div> | |
| </div> | |
| `; | |
| // Update button to Cancel | |
| if (manageBtn) manageBtn.innerHTML = '<i class="fa-solid fa-xmark"></i> Cancel'; | |
| // Hide Enable/Disable button in expanded mode | |
| const enableDisableBtn = actionsDiv ? actionsDiv.querySelector('[onclick^="toggleUserMgmtStatus"]') : null; | |
| if (enableDisableBtn) enableDisableBtn.style.display = 'none'; | |
| activeExpandedId = userId; | |
| } else { | |
| // Collapse this card | |
| console.log('Collapsing card...'); | |
| card.classList.remove('expanded'); | |
| metaDiv.style.display = 'flex'; | |
| expansionDiv.style.display = 'none'; | |
| expansionDiv.innerHTML = ''; | |
| // Update button back to Manage Access | |
| if (manageBtn) manageBtn.innerHTML = '<i class="fa-solid fa-gears"></i> Manage Access'; | |
| // Show Enable/Disable button again | |
| const enableDisableBtn = actionsDiv ? actionsDiv.querySelector('[onclick^="toggleUserMgmtStatus"]') : null; | |
| if (enableDisableBtn) enableDisableBtn.style.display = 'flex'; | |
| activeExpandedId = null; | |
| } | |
| } | |
| async function toggleUserMgmtStatus(userId) { | |
| try { | |
| const res = await fetch(`/api/admin/users/${userId}/toggle-status`, { method: 'POST' }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| // Collapse any expanded card and refresh list | |
| activeExpandedId = null; | |
| loadUserManagement(); | |
| if (typeof showToast === 'function') showToast('success', 'Status Updated', 'User access level has been modified.'); | |
| } else { | |
| if (typeof showToast === 'function') showToast('error', 'Action Denied', data.error || 'Failed to update status.'); | |
| } | |
| } catch (e) { | |
| if (typeof showToast === 'function') showToast('error', 'Network Error', 'Failed to connect to server.'); | |
| } | |
| } | |
| async function updateUserMgmtPassword(userId) { | |
| const passInput = document.getElementById(`new-pass-${userId}`); | |
| const newPass = passInput ? passInput.value.trim() : ''; | |
| if (!newPass) { | |
| alert('Please enter a password.'); | |
| return; | |
| } | |
| try { | |
| const res = await fetch(`/api/admin/users/${userId}/change-password`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ new_password: newPass }) | |
| }); | |
| if (res.ok) { | |
| passInput.value = ''; | |
| if (typeof showToast === 'function') showToast('success', 'Security Update', 'User password has been manually overridden.'); | |
| } | |
| } catch (e) {} | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CHANGE PASSWORD MODAL LOGIC | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| let changePassTargetId = null; | |
| function openChangePasswordModal(userId) { | |
| changePassTargetId = userId; | |
| const modal = document.getElementById('user-change-pass-modal'); | |
| const newPassEl = document.getElementById('change-pass-new'); | |
| const errorEl = document.getElementById('change-pass-error'); | |
| // Get the password from the input field | |
| const passInput = document.getElementById(`new-pass-${userId}`); | |
| const newPassword = passInput ? passInput.value : ''; | |
| if (!newPassword) { | |
| if (typeof showToast === 'function') showToast('error', 'Error', 'Please enter a new password first.'); | |
| return; | |
| } | |
| if (newPassEl) newPassEl.value = newPassword; | |
| if (errorEl) errorEl.innerText = ''; | |
| if (modal) modal.style.display = 'flex'; | |
| // Set up the confirm button | |
| const finalBtn = document.getElementById('final-change-pass-btn'); | |
| if (finalBtn) { | |
| finalBtn.onclick = confirmChangePassword; | |
| } | |
| } | |
| function closeChangePasswordModal() { | |
| const modal = document.getElementById('user-change-pass-modal'); | |
| if (modal) modal.style.display = 'none'; | |
| changePassTargetId = null; | |
| } | |
| async function confirmChangePassword() { | |
| if (!changePassTargetId) return; | |
| const newPassEl = document.getElementById('change-pass-new'); | |
| const errorEl = document.getElementById('change-pass-error'); | |
| const newPassword = newPassEl ? newPassEl.value : ''; | |
| if (errorEl) errorEl.innerText = 'Updating password...'; | |
| try { | |
| const res = await fetch(`/api/admin/users/${changePassTargetId}/change-password`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ new_password: newPassword }) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| closeChangePasswordModal(); | |
| // Clear the password input | |
| const passInput = document.getElementById(`new-pass-${changePassTargetId}`); | |
| if (passInput) passInput.value = ''; | |
| if (typeof showToast === 'function') showToast('success', 'Success', 'User password has been changed.'); | |
| } else { | |
| if (errorEl) errorEl.innerText = data.error || 'Failed to change password.'; | |
| } | |
| } catch (e) { | |
| if (errorEl) errorEl.innerText = 'Network error occurred.'; | |
| } | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SECURE DELETION LOGIC | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function openDeleteUserModal(id, name) { | |
| deleteTargetId = id; | |
| const modal = document.getElementById('user-delete-modal'); | |
| const nameEl = document.getElementById('delete-target-name'); | |
| const errorEl = document.getElementById('delete-error'); | |
| const passInput = document.getElementById('admin-auth-pass'); | |
| if (nameEl) nameEl.innerText = name; | |
| if (errorEl) errorEl.innerText = ''; | |
| if (passInput) passInput.value = ''; | |
| if (modal) modal.style.display = 'flex'; | |
| // Set up the final button | |
| const finalBtn = document.getElementById('final-delete-btn'); | |
| if (finalBtn) { | |
| finalBtn.onclick = confirmDeleteUser; | |
| } | |
| } | |
| function closeDeleteModal() { | |
| const modal = document.getElementById('user-delete-modal'); | |
| if (modal) modal.style.display = 'none'; | |
| deleteTargetId = null; | |
| } | |
| async function confirmDeleteUser() { | |
| if (!deleteTargetId) return; | |
| const passInput = document.getElementById('admin-auth-pass'); | |
| const adminPass = passInput ? passInput.value : ''; | |
| const errorEl = document.getElementById('delete-error'); | |
| if (!adminPass) { | |
| if (errorEl) errorEl.innerText = 'Authorization required.'; | |
| return; | |
| } | |
| if (errorEl) errorEl.innerText = 'Deleting account...'; | |
| try { | |
| const res = await fetch(`/api/admin/users/${deleteTargetId}/delete`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ admin_password: adminPass }) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| // Show success toast first | |
| if (typeof showToast === 'function') showToast('success', 'Success', 'User deleted successfully.'); | |
| // Wait 1 second before closing modal and refreshing | |
| setTimeout(() => { | |
| closeDeleteModal(); | |
| loadUserManagement(); | |
| }, 1000); | |
| } else { | |
| if (errorEl) errorEl.innerText = data.error || 'Authorization failed.'; | |
| if (typeof showToast === 'function') showToast('error', 'Error', data.error || 'Failed to delete user.'); | |
| } | |
| } catch (e) { | |
| if (errorEl) errorEl.innerText = 'Network error occurred.'; | |
| } | |
| } | |
| // Helper: Escape HTML | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } | |