595 lines
23 KiB
JavaScript
595 lines
23 KiB
JavaScript
/**
|
|
* Admin Users Management Module
|
|
* Handles user CRUD operations, email broadcasting, and user administration
|
|
*/
|
|
|
|
// User management state
|
|
let allUsersData = [];
|
|
let filteredUsersData = [];
|
|
let currentSearchTerm = '';
|
|
let currentFilterType = 'all';
|
|
let eventListenersSetup = false;
|
|
let usersInitialized = false;
|
|
|
|
// User Management Functions
|
|
async function loadUsers() {
|
|
const loadingEl = document.getElementById('users-loading');
|
|
const emptyEl = document.getElementById('users-empty');
|
|
const tableBody = document.getElementById('users-table-body');
|
|
|
|
if (loadingEl) loadingEl.style.display = 'block';
|
|
if (emptyEl) emptyEl.style.display = 'none';
|
|
if (tableBody) tableBody.innerHTML = '';
|
|
|
|
try {
|
|
const response = await fetch('/api/users');
|
|
const data = await response.json();
|
|
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
|
|
if (data.success && data.users) {
|
|
allUsersData = data.users;
|
|
filteredUsersData = [...allUsersData];
|
|
displayUsers(filteredUsersData);
|
|
updateSearchResults();
|
|
|
|
// Make sure event listeners are set up after loading users
|
|
if (!eventListenersSetup) {
|
|
setupUserEventListeners();
|
|
}
|
|
|
|
usersInitialized = true;
|
|
} else {
|
|
throw new Error(data.error || 'Failed to load users');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading users:', error);
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
if (emptyEl) {
|
|
emptyEl.textContent = 'Failed to load users';
|
|
emptyEl.style.display = 'block';
|
|
}
|
|
window.adminCore.showStatus('Failed to load users', 'error');
|
|
}
|
|
}
|
|
|
|
function displayUsers(users) {
|
|
const container = document.querySelector('.users-list');
|
|
if (!container) return;
|
|
|
|
// Find or create the users table container, preserving the header
|
|
let usersTableContainer = container.querySelector('.users-table-container');
|
|
if (!usersTableContainer) {
|
|
// If container doesn't exist, create it after the header
|
|
const header = container.querySelector('.users-list-header');
|
|
usersTableContainer = document.createElement('div');
|
|
usersTableContainer.className = 'users-table-container';
|
|
|
|
if (header && header.nextSibling) {
|
|
container.insertBefore(usersTableContainer, header.nextSibling);
|
|
} else if (header) {
|
|
container.appendChild(usersTableContainer);
|
|
} else {
|
|
container.appendChild(usersTableContainer);
|
|
}
|
|
}
|
|
|
|
if (!users || users.length === 0) {
|
|
const hasFilter = currentSearchTerm || currentFilterType !== 'all';
|
|
const emptyMessage = hasFilter ?
|
|
'No users match your search criteria.' :
|
|
'No users found.';
|
|
usersTableContainer.innerHTML = `<div class="no-users-found">${emptyMessage}</div>`;
|
|
return;
|
|
}
|
|
|
|
const cardsHtml = `
|
|
<div class="users-cards-container">
|
|
${users.map(user => {
|
|
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
|
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
|
const isAdmin = user.admin || user.Admin || false;
|
|
const userType = user.UserType || user.userType || user['User Type'] || (isAdmin ? 'admin' : 'user');
|
|
const userId = user.Id || user.id || user.ID;
|
|
|
|
// Handle expiration info
|
|
let expirationInfo = '';
|
|
if (user.ExpiresAt) {
|
|
const expirationDate = new Date(user.ExpiresAt);
|
|
const now = new Date();
|
|
const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));
|
|
|
|
if (daysUntilExpiration < 0) {
|
|
expirationInfo = `<span class="expiration-info expiration-warning">Expired ${Math.abs(daysUntilExpiration)} days ago</span>`;
|
|
} else if (daysUntilExpiration <= 3) {
|
|
expirationInfo = `<span class="expiration-info expiration-warning">Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}</span>`;
|
|
} else {
|
|
expirationInfo = `<span class="expiration-info">Expires: ${expirationDate.toLocaleDateString()}</span>`;
|
|
}
|
|
}
|
|
|
|
const cardClass = user.ExpiresAt && new Date(user.ExpiresAt) < new Date() ? 'user-card expired' :
|
|
(user.ExpiresAt && new Date(user.ExpiresAt) - new Date() < 3 * 24 * 60 * 60 * 1000 ? 'user-card expires-soon' : 'user-card');
|
|
|
|
return `
|
|
<div class="${cardClass}">
|
|
<div class="user-card-header">
|
|
<div class="user-info-primary">
|
|
<h4 class="user-name">${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')}</h4>
|
|
<p class="user-email">${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')}</p>
|
|
</div>
|
|
<div class="user-role-badge">
|
|
<span class="user-role ${userType}">
|
|
${userType.charAt(0).toUpperCase() + userType.slice(1)}
|
|
</span>
|
|
${expirationInfo}
|
|
</div>
|
|
</div>
|
|
<div class="user-card-body">
|
|
<div class="user-details-compact">
|
|
📞 ${window.adminCore.escapeHtml(user.phone || user.Phone || 'No phone')} • 📅 ${formattedDate}
|
|
</div>
|
|
</div>
|
|
<div class="user-card-footer">
|
|
<div class="user-actions">
|
|
<div class="user-communication-actions">
|
|
<a href="mailto:${window.adminCore.escapeHtml(user.email || user.Email)}"
|
|
class="btn btn-sm btn-outline-primary"
|
|
title="Email ${window.adminCore.escapeHtml(user.name || user.Name || 'User')}">
|
|
📧
|
|
</a>
|
|
<a href="sms:${window.adminCore.escapeHtml(user.phone || user.Phone || '')}"
|
|
class="btn btn-sm btn-outline-secondary ${!(user.phone || user.Phone) ? 'disabled' : ''}"
|
|
title="Text ${window.adminCore.escapeHtml(user.name || user.Name || 'User')}${!(user.phone || user.Phone) ? ' (No phone number)' : ''}">
|
|
💬
|
|
</a>
|
|
<a href="tel:${window.adminCore.escapeHtml(user.phone || user.Phone || '')}"
|
|
class="btn btn-sm btn-outline-success ${!(user.phone || user.Phone) ? 'disabled' : ''}"
|
|
title="Call ${window.adminCore.escapeHtml(user.name || user.Name || 'User')}${!(user.phone || user.Phone) ? ' (No phone number)' : ''}">
|
|
📞
|
|
</a>
|
|
</div>
|
|
<div class="user-admin-actions">
|
|
<button class="btn btn-secondary btn-sm send-login-btn" data-user-id="${userId}" data-user-email="${window.adminCore.escapeHtml(user.email || user.Email)}">
|
|
Send Login
|
|
</button>
|
|
<button class="btn btn-sm delete-user-btn" data-user-id="${userId}" data-user-email="${window.adminCore.escapeHtml(user.email || user.Email)}" title="Delete User">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
|
|
`;
|
|
|
|
usersTableContainer.innerHTML = cardsHtml;
|
|
setupUserActionListeners();
|
|
}
|
|
|
|
// Keep for backward compatibility
|
|
function searchUsers(searchTerm, filterType = 'all') {
|
|
// Update the form inputs and call filterUsers
|
|
const searchInput = document.getElementById('users-search');
|
|
const filterSelect = document.getElementById('users-filter-type');
|
|
|
|
if (searchInput) searchInput.value = searchTerm;
|
|
if (filterSelect) filterSelect.value = filterType;
|
|
|
|
filterUsers();
|
|
}
|
|
|
|
function updateSearchResults() {
|
|
const resultsElement = document.getElementById('users-search-results');
|
|
const resultsText = document.querySelector('.search-results-text');
|
|
|
|
if (!resultsElement || !resultsText) return;
|
|
|
|
// Get current values from form
|
|
const searchInput = document.getElementById('users-search');
|
|
const filterSelect = document.getElementById('users-filter-type');
|
|
|
|
const searchTerm = searchInput ? searchInput.value.trim() : '';
|
|
const filterType = filterSelect ? filterSelect.value : 'all';
|
|
|
|
const hasFilter = searchTerm || filterType !== 'all';
|
|
|
|
if (hasFilter) {
|
|
resultsElement.style.display = 'block';
|
|
|
|
let message = `Showing ${filteredUsersData.length} of ${allUsersData.length} users`;
|
|
|
|
if (searchTerm && filterType !== 'all') {
|
|
message += ` matching "${searchTerm}" in ${filterType} accounts`;
|
|
} else if (searchTerm) {
|
|
message += ` matching "${searchTerm}"`;
|
|
} else if (filterType !== 'all') {
|
|
message += ` with ${filterType} accounts`;
|
|
}
|
|
|
|
resultsText.textContent = message;
|
|
} else {
|
|
resultsElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function clearSearch() {
|
|
const searchInput = document.getElementById('users-search');
|
|
const filterSelect = document.getElementById('users-filter-type');
|
|
|
|
if (searchInput) searchInput.value = '';
|
|
if (filterSelect) filterSelect.value = 'all';
|
|
|
|
filterUsers();
|
|
}
|
|
|
|
function setupUserActionListeners() {
|
|
const container = document.querySelector('.users-list');
|
|
if (!container) return;
|
|
|
|
// Use event delegation instead of replacing containers
|
|
// This avoids destroying search controls and their event listeners
|
|
|
|
// Remove any existing click listeners on the container to avoid duplicates
|
|
if (container._userActionHandler) {
|
|
container.removeEventListener('click', container._userActionHandler);
|
|
}
|
|
|
|
// Create the new handler
|
|
const newHandler = function(e) {
|
|
if (e.target.classList.contains('delete-user-btn')) {
|
|
const userId = e.target.getAttribute('data-user-id');
|
|
const userEmail = e.target.getAttribute('data-user-email');
|
|
console.log('Delete button clicked for user:', userId);
|
|
deleteUser(userId, userEmail);
|
|
} else if (e.target.classList.contains('send-login-btn')) {
|
|
const userId = e.target.getAttribute('data-user-id');
|
|
const userEmail = e.target.getAttribute('data-user-email');
|
|
console.log('Send login details button clicked for user:', userId);
|
|
sendLoginDetailsToUser(userId, userEmail);
|
|
} else if (e.target.id === 'email-all-users-btn') {
|
|
console.log('Email All Users button clicked');
|
|
showEmailUsersModal();
|
|
}
|
|
};
|
|
|
|
// Add the new handler and store reference for removal later
|
|
container.addEventListener('click', newHandler);
|
|
container._userActionHandler = newHandler;
|
|
}
|
|
|
|
async function deleteUser(userId, userEmail) {
|
|
if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/users/${userId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
window.adminCore.showStatus(`User "${userEmail}" deleted successfully`, 'success');
|
|
loadUsers(); // Reload the users list
|
|
} else {
|
|
throw new Error(data.error || 'Failed to delete user');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
window.adminCore.showStatus(`Failed to delete user: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function sendLoginDetailsToUser(userId, userEmail) {
|
|
if (!confirm(`Send login details to "${userEmail}"?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/users/${userId}/send-login-details`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
window.adminCore.showStatus(`Login details sent to "${userEmail}" successfully`, 'success');
|
|
} else {
|
|
throw new Error(data.error || 'Failed to send login details');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error sending login details:', error);
|
|
window.adminCore.showStatus(`Failed to send login details: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function createUser(e) {
|
|
e.preventDefault();
|
|
|
|
const emailInput = document.getElementById('user-email');
|
|
const passwordInput = document.getElementById('user-password');
|
|
const nameInput = document.getElementById('user-name');
|
|
const phoneInput = document.getElementById('user-phone');
|
|
const userTypeSelect = document.getElementById('user-type');
|
|
const expireDaysInput = document.getElementById('user-expire-days');
|
|
const adminCheckbox = document.getElementById('user-is-admin');
|
|
|
|
const email = emailInput?.value.trim();
|
|
const password = passwordInput?.value;
|
|
const name = nameInput?.value.trim();
|
|
const phone = phoneInput?.value.trim();
|
|
const userType = userTypeSelect?.value;
|
|
const expireDays = userType === 'temp' ?
|
|
parseInt(expireDaysInput?.value) : null;
|
|
const admin = adminCheckbox?.checked;
|
|
|
|
if (!email || !password) {
|
|
window.adminCore.showStatus('Email and password are required', 'error');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
window.adminCore.showStatus('Password must be at least 6 characters long', 'error');
|
|
return;
|
|
}
|
|
|
|
if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) {
|
|
window.adminCore.showStatus('Expiration days must be between 1 and 365 for temporary users', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userData = {
|
|
email,
|
|
password,
|
|
name: name || '',
|
|
phone: phone || '',
|
|
isAdmin: userType === 'admin' || admin,
|
|
userType,
|
|
expireDays
|
|
};
|
|
|
|
const response = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(userData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
window.adminCore.showStatus('User created successfully', 'success');
|
|
clearUserForm();
|
|
loadUsers(); // Reload the users list
|
|
} else {
|
|
throw new Error(data.error || 'Failed to create user');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error creating user:', error);
|
|
window.adminCore.showStatus(`Failed to create user: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function clearUserForm() {
|
|
const form = document.getElementById('create-user-form');
|
|
if (form) {
|
|
form.reset();
|
|
|
|
// Reset user type to default
|
|
const userTypeSelect = document.getElementById('user-type');
|
|
if (userTypeSelect) {
|
|
userTypeSelect.value = 'user';
|
|
}
|
|
|
|
// Hide expiration group
|
|
const expirationGroup = document.getElementById('expiration-group');
|
|
if (expirationGroup) {
|
|
expirationGroup.style.display = 'none';
|
|
}
|
|
|
|
// Re-enable admin checkbox
|
|
const isAdminCheckbox = document.getElementById('user-is-admin');
|
|
if (isAdminCheckbox) {
|
|
isAdminCheckbox.disabled = false;
|
|
}
|
|
|
|
window.adminCore.showStatus('User form cleared', 'info');
|
|
}
|
|
}
|
|
|
|
// Simple search function like cuts
|
|
function filterUsers() {
|
|
const searchTerm = document.getElementById('users-search').value.toLowerCase();
|
|
const filterType = document.getElementById('users-filter-type').value;
|
|
|
|
let filteredUsers = allUsersData;
|
|
|
|
// Apply type filter first
|
|
if (filterType !== 'all') {
|
|
filteredUsers = filteredUsers.filter(user => {
|
|
const userType = user.UserType || user.userType || user['User Type'] ||
|
|
(user.admin || user.Admin ? 'admin' : 'user');
|
|
return userType === filterType;
|
|
});
|
|
}
|
|
|
|
// Apply search filter
|
|
if (searchTerm) {
|
|
filteredUsers = filteredUsers.filter(user => {
|
|
// Handle different possible field names and null/undefined values
|
|
const userName = user.name || user.Name || '';
|
|
const userEmail = user.email || user.Email || '';
|
|
const userPhone = user.phone || user.Phone || '';
|
|
|
|
// Prioritize name matches - if name matches, return true immediately
|
|
if (userName.toLowerCase().includes(searchTerm)) {
|
|
return true;
|
|
}
|
|
|
|
// Then check email and phone
|
|
return userEmail.toLowerCase().includes(searchTerm) ||
|
|
userPhone.toLowerCase().includes(searchTerm);
|
|
});
|
|
|
|
// Sort results to prioritize name matches at the top
|
|
filteredUsers.sort((a, b) => {
|
|
const nameA = a.name || a.Name || '';
|
|
const nameB = b.name || b.Name || '';
|
|
const nameAMatches = nameA.toLowerCase().includes(searchTerm);
|
|
const nameBMatches = nameB.toLowerCase().includes(searchTerm);
|
|
|
|
// If both match by name or both don't match by name, maintain original order
|
|
if (nameAMatches === nameBMatches) {
|
|
return 0;
|
|
}
|
|
|
|
// Prioritize name matches (true comes before false)
|
|
return nameBMatches - nameAMatches;
|
|
});
|
|
}
|
|
|
|
filteredUsersData = filteredUsers;
|
|
displayUsers(filteredUsersData);
|
|
updateSearchResults();
|
|
}
|
|
|
|
// Setup user-related event listeners
|
|
function setupUserEventListeners() {
|
|
// Prevent duplicate event listener setup
|
|
if (eventListenersSetup) {
|
|
console.log('User event listeners already set up, skipping');
|
|
return;
|
|
}
|
|
|
|
console.log('Setting up user event listeners...');
|
|
|
|
// User form submission
|
|
const userForm = document.getElementById('create-user-form');
|
|
if (userForm) {
|
|
userForm.addEventListener('submit', createUser);
|
|
}
|
|
|
|
// Clear user form button
|
|
const clearUserBtn = document.getElementById('clear-user-form');
|
|
if (clearUserBtn) {
|
|
clearUserBtn.addEventListener('click', clearUserForm);
|
|
}
|
|
|
|
// User type change listener
|
|
const userTypeSelect = document.getElementById('user-type');
|
|
if (userTypeSelect) {
|
|
userTypeSelect.addEventListener('change', (e) => {
|
|
const expirationGroup = document.getElementById('expiration-group');
|
|
const isAdminCheckbox = document.getElementById('user-is-admin');
|
|
|
|
if (e.target.value === 'temp') {
|
|
if (expirationGroup) expirationGroup.style.display = 'block';
|
|
if (isAdminCheckbox) {
|
|
isAdminCheckbox.checked = false;
|
|
isAdminCheckbox.disabled = true;
|
|
}
|
|
} else {
|
|
if (expirationGroup) expirationGroup.style.display = 'none';
|
|
if (isAdminCheckbox) isAdminCheckbox.disabled = false;
|
|
|
|
if (e.target.value === 'admin') {
|
|
if (isAdminCheckbox) isAdminCheckbox.checked = true;
|
|
} else {
|
|
if (isAdminCheckbox) isAdminCheckbox.checked = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set up search and filter (simple like cuts manager)
|
|
const searchInput = document.getElementById('users-search');
|
|
if (searchInput) {
|
|
console.log('✅ Setting up users search input listener');
|
|
searchInput.addEventListener('input', () => filterUsers());
|
|
} else {
|
|
console.log('⚠️ users-search input not found');
|
|
}
|
|
|
|
const filterSelect = document.getElementById('users-filter-type');
|
|
if (filterSelect) {
|
|
console.log('✅ Setting up users filter select listener');
|
|
filterSelect.addEventListener('change', () => filterUsers());
|
|
} else {
|
|
console.log('⚠️ users-filter-type select not found');
|
|
}
|
|
|
|
// Clear search button
|
|
const clearSearchBtn = document.getElementById('clear-users-search');
|
|
if (clearSearchBtn) {
|
|
clearSearchBtn.addEventListener('click', clearSearch);
|
|
}
|
|
|
|
// Mark event listeners as set up
|
|
eventListenersSetup = true;
|
|
}
|
|
|
|
// Debug function to test search manually
|
|
function debugUserSearch(testTerm = 'test') {
|
|
console.log('=== DEBUG USER SEARCH ===');
|
|
console.log('All users count:', allUsersData.length);
|
|
console.log('Current search term:', currentSearchTerm);
|
|
console.log('Current filter type:', currentFilterType);
|
|
console.log('Filtered users count:', filteredUsersData.length);
|
|
|
|
// Set test term in search input and trigger filter
|
|
const searchInput = document.getElementById('users-search');
|
|
const filterSelect = document.getElementById('users-filter-type');
|
|
if (searchInput) searchInput.value = testTerm;
|
|
if (filterSelect) filterSelect.value = 'all';
|
|
|
|
console.log('Testing search with term:', testTerm);
|
|
filterUsers();
|
|
|
|
console.log('After search - Filtered users count:', filteredUsersData.length);
|
|
console.log('========================');
|
|
|
|
return {
|
|
allUsers: allUsersData.length,
|
|
filteredUsers: filteredUsersData.length,
|
|
searchTerm: currentSearchTerm,
|
|
filterType: currentFilterType
|
|
};
|
|
}
|
|
|
|
// Export user management functions
|
|
window.adminUsers = {
|
|
loadUsers,
|
|
displayUsers,
|
|
deleteUser,
|
|
sendLoginDetailsToUser,
|
|
createUser,
|
|
clearUserForm,
|
|
setupUserEventListeners,
|
|
searchUsers,
|
|
filterUsers,
|
|
clearSearch,
|
|
updateSearchResults,
|
|
debugUserSearch,
|
|
getAllUsersData: () => allUsersData,
|
|
getFilteredUsersData: () => filteredUsersData,
|
|
setAllUsersData: (data) => {
|
|
allUsersData = data;
|
|
filteredUsersData = [...allUsersData];
|
|
},
|
|
get isInitialized() { return usersInitialized; }
|
|
};
|