mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-05 21:19:42 -06:00
This release introduces several key features and improvements
Added:
- Account email change support from Settings with full validation and UI update.
- Progressive Web App (PWA) capability with manifest and service worker integration.
- "Manage Tags" button on the main page to streamline tag management UX.
Changed:
- Improved responsiveness of key pages (index, status) across mobile and tablet views.
- Replaced hardcoded DB credentials with environment variable references (contribution by @humrochagf, commit 20997e9):
• Credentials are now pulled from Docker Compose env vars.
• Redundant superuser credential checks removed.
• Updated migration and permission scripts for dynamic configuration.
This version enhances user flexibility, security, and cross-device compatibility.
3024 lines
118 KiB
JavaScript
3024 lines
118 KiB
JavaScript
// DOM Elements
|
||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||
const darkModeToggleSetting = document.getElementById('darkModeToggleSetting');
|
||
const defaultViewSelect = document.getElementById('defaultView');
|
||
const emailNotificationsToggle = document.getElementById('emailNotifications');
|
||
const expiringSoonDaysInput = document.getElementById('expiringSoonDays');
|
||
const notificationFrequencySelect = document.getElementById('notificationFrequency');
|
||
const notificationTimeInput = document.getElementById('notificationTime');
|
||
const timezoneSelect = document.getElementById('timezone');
|
||
const saveProfileBtn = document.getElementById('saveProfileBtn');
|
||
const savePreferencesBtn = document.getElementById('savePreferencesBtn');
|
||
const saveEmailSettingsBtn = document.getElementById('saveEmailSettingsBtn');
|
||
const changePasswordBtn = document.getElementById('changePasswordBtn');
|
||
const passwordChangeForm = document.getElementById('passwordChangeForm');
|
||
const savePasswordBtn = document.getElementById('savePasswordBtn');
|
||
const cancelPasswordBtn = document.getElementById('cancelPasswordBtn');
|
||
const deleteAccountBtn = document.getElementById('deleteAccountBtn');
|
||
const deleteAccountModal = document.getElementById('deleteAccountModal');
|
||
const deleteConfirmInput = document.getElementById('deleteConfirmInput');
|
||
const confirmDeleteAccountBtn = document.getElementById('confirmDeleteAccountBtn');
|
||
const passwordSuccessModal = document.getElementById('passwordSuccessModal');
|
||
const loadingContainer = document.getElementById('loadingContainer');
|
||
const toastContainer = document.getElementById('toastContainer');
|
||
const settingsBtn = document.getElementById('settingsBtn');
|
||
const settingsMenu = document.getElementById('settingsMenu');
|
||
const usersTableBody = document.getElementById('usersTableBody');
|
||
|
||
// Form fields
|
||
const firstNameInput = document.getElementById('firstName');
|
||
const lastNameInput = document.getElementById('lastName');
|
||
const emailInput = document.getElementById('email');
|
||
const currentPasswordInput = document.getElementById('currentPassword');
|
||
const newPasswordInput = document.getElementById('newPassword');
|
||
const confirmPasswordInput = document.getElementById('confirmPassword');
|
||
|
||
// DOM Elements for admin section
|
||
const adminSection = document.getElementById('adminSection');
|
||
const refreshUsersBtn = document.getElementById('refreshUsersBtn');
|
||
const checkAdminBtn = document.getElementById('checkAdminBtn');
|
||
const showUsersBtn = document.getElementById('showUsersBtn');
|
||
const testApiBtn = document.getElementById('testApiBtn');
|
||
const triggerNotificationsBtn = document.getElementById('triggerNotificationsBtn');
|
||
const registrationEnabled = document.getElementById('registrationEnabled');
|
||
const saveSiteSettingsBtn = document.getElementById('saveSiteSettingsBtn');
|
||
const emailBaseUrlInput = document.getElementById('emailBaseUrl'); // Added for email base URL
|
||
const currencySymbolInput = document.getElementById('currencySymbol');
|
||
const currencySymbolSelect = document.getElementById('currencySymbolSelect');
|
||
const currencySymbolCustom = document.getElementById('currencySymbolCustom');
|
||
|
||
// Add dateFormatSelect near other DOM element declarations if not already there
|
||
const dateFormatSelect = document.getElementById('dateFormat');
|
||
|
||
/**
|
||
* Set theme (dark/light) - Unified and persistent
|
||
* @param {boolean} isDark - Whether to use dark mode
|
||
*/
|
||
function setTheme(isDark) {
|
||
const theme = isDark ? 'dark' : 'light';
|
||
// Apply theme to document
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
if (isDark) {
|
||
document.body.classList.add('dark-mode');
|
||
} else {
|
||
document.body.classList.remove('dark-mode');
|
||
}
|
||
// Save to localStorage (single source of truth)
|
||
localStorage.setItem('darkMode', isDark);
|
||
// Sync both toggles if present
|
||
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) {
|
||
darkModeToggle.checked = isDark;
|
||
}
|
||
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) {
|
||
darkModeToggleSetting.checked = isDark;
|
||
}
|
||
// Also update user_preferences.theme for backward compatibility
|
||
try {
|
||
let userPrefs = {};
|
||
const storedPrefs = localStorage.getItem('user_preferences');
|
||
if (storedPrefs) {
|
||
userPrefs = JSON.parse(storedPrefs);
|
||
}
|
||
userPrefs.theme = theme;
|
||
localStorage.setItem('user_preferences', JSON.stringify(userPrefs));
|
||
} catch (e) {
|
||
console.error('Error updating theme in user_preferences:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Initialize dark mode toggle and synchronize state
|
||
*/
|
||
function initDarkModeToggle() {
|
||
// Always check the single source of truth in localStorage (fallback)
|
||
const isDarkMode = localStorage.getItem('darkMode') === 'true';
|
||
// Apply theme to DOM if not already set
|
||
document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
|
||
document.body.classList.toggle('dark-mode', isDarkMode);
|
||
// Sync both toggles and add unified handler
|
||
const syncToggles = (val) => {
|
||
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) darkModeToggle.checked = val;
|
||
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) darkModeToggleSetting.checked = val;
|
||
};
|
||
syncToggles(isDarkMode);
|
||
// Handler to update theme, localStorage, backend
|
||
const handleToggle = async function(checked) {
|
||
setTheme(checked);
|
||
syncToggles(checked);
|
||
// Save to backend if authenticated
|
||
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
|
||
try {
|
||
let prefs = {};
|
||
const storedPrefs = localStorage.getItem('user_preferences');
|
||
if (storedPrefs) prefs = JSON.parse(storedPrefs);
|
||
prefs.theme = checked ? 'dark' : 'light';
|
||
await fetch('/api/auth/preferences', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(prefs)
|
||
});
|
||
} catch (e) {
|
||
console.warn('Failed to save dark mode to backend:', e);
|
||
}
|
||
}
|
||
};
|
||
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) {
|
||
darkModeToggle.onchange = function() { handleToggle(this.checked); };
|
||
}
|
||
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) {
|
||
darkModeToggleSetting.onchange = function() { handleToggle(this.checked); };
|
||
}
|
||
}
|
||
|
||
// Initialize settings page
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('DOM fully loaded, initializing settings page');
|
||
|
||
// Set up event listeners
|
||
setupEventListeners(); // Ensure this doesn't also try to init settings menu
|
||
|
||
// Set up direct event listeners for critical elements
|
||
setupCriticalEventListeners();
|
||
|
||
// Make functions globally accessible if needed
|
||
window.deleteUser = deleteUser;
|
||
window.directDeleteUserAPI = directDeleteUserAPI;
|
||
|
||
// Add global click handlers if needed
|
||
// ... (existing delete button handler logic) ...
|
||
|
||
// Initialize dark mode toggle
|
||
initDarkModeToggle();
|
||
// Clear dark mode preference on logout for privacy
|
||
if (window.auth && window.auth.onLogout) {
|
||
window.auth.onLogout(() => {
|
||
localStorage.removeItem('darkMode');
|
||
});
|
||
}
|
||
|
||
// REMOVED initSettingsMenu() call - Handled by auth.js
|
||
|
||
// Load initial data for the settings page
|
||
loadUserData();
|
||
loadTimezones().then(() => loadPreferences()).catch(err => {
|
||
console.error('Error loading timezones/prefs:', err);
|
||
loadPreferences(); // Try loading prefs anyway
|
||
});
|
||
|
||
// --- ADD THIS LINE TO INITIALIZE MODALS ---
|
||
initModals();
|
||
});
|
||
|
||
/**
|
||
* Initialize the settings page
|
||
*/
|
||
function initPage() {
|
||
console.log('Initializing settings page');
|
||
|
||
// Check authentication
|
||
if (window.auth) {
|
||
window.auth.checkAuthState();
|
||
|
||
// Redirect to login if not authenticated
|
||
if (!window.auth.isAuthenticated()) {
|
||
console.log('User not authenticated, redirecting to login');
|
||
window.location.href = 'login.html';
|
||
return;
|
||
}
|
||
} else {
|
||
// Auth module not loaded
|
||
console.error('Auth module not loaded');
|
||
window.location.href = 'login.html';
|
||
return;
|
||
}
|
||
|
||
// Load user data and preferences with error handling
|
||
try {
|
||
// First load timezones, then load preferences to ensure correct order
|
||
loadTimezones().then(() => {
|
||
console.log('Timezones loaded, now loading preferences');
|
||
loadPreferences();
|
||
}).catch(err => {
|
||
console.error('Error loading timezones:', err);
|
||
// Still try to load preferences even if timezones fail
|
||
loadPreferences();
|
||
});
|
||
|
||
loadUserData().catch(err => {
|
||
console.error('Error loading user data:', err);
|
||
// Continue with page initialization even if user data fails
|
||
});
|
||
} catch (err) {
|
||
console.error('Error during page initialization:', err);
|
||
// Continue despite errors to allow basic functionality
|
||
}
|
||
|
||
// Fix for settings button - not needed anymore as we've removed it
|
||
if (settingsBtn) {
|
||
console.log('Settings button found, adding event listener');
|
||
settingsBtn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
settingsMenu.classList.toggle('active');
|
||
console.log('Settings button clicked, menu toggled');
|
||
});
|
||
|
||
// Close settings menu when clicking outside
|
||
document.addEventListener('click', function(e) {
|
||
if (settingsMenu.classList.contains('active') &&
|
||
!settingsMenu.contains(e.target) &&
|
||
!settingsBtn.contains(e.target)) {
|
||
settingsMenu.classList.remove('active');
|
||
}
|
||
});
|
||
} else {
|
||
console.log('Settings button not found - this is expected after UI update');
|
||
}
|
||
|
||
console.log('Settings page initialization complete');
|
||
}
|
||
|
||
/**
|
||
* Setup critical event listeners that must work for core functionality
|
||
*/
|
||
function setupCriticalEventListeners() {
|
||
console.log('Setting up critical event listeners');
|
||
|
||
// Set up delete user modal close buttons
|
||
const closeModalButtons = document.querySelectorAll('.close-modal');
|
||
closeModalButtons.forEach(button => {
|
||
button.addEventListener('click', function() {
|
||
closeAllModals();
|
||
});
|
||
});
|
||
|
||
// Set up delete user button in the modal
|
||
const confirmDeleteUserBtn = document.getElementById('confirmDeleteUserBtn');
|
||
if (confirmDeleteUserBtn) {
|
||
console.log('Setting up confirmDeleteUserBtn');
|
||
|
||
// Add multiple event handlers for redundancy
|
||
confirmDeleteUserBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked via addEventListener in setupCriticalEventListeners');
|
||
deleteUser();
|
||
return false;
|
||
});
|
||
|
||
confirmDeleteUserBtn.onclick = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked via onclick in setupCriticalEventListeners');
|
||
deleteUser();
|
||
return false;
|
||
};
|
||
}
|
||
|
||
// Set up direct delete link
|
||
const directDeleteLink = document.getElementById('directDeleteLink');
|
||
if (directDeleteLink) {
|
||
directDeleteLink.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Direct delete link clicked in setupCriticalEventListeners');
|
||
deleteUser();
|
||
return false;
|
||
});
|
||
}
|
||
|
||
// Set up direct API link
|
||
const directAPILink = document.getElementById('directAPILink');
|
||
if (directAPILink) {
|
||
directAPILink.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Direct API link clicked in setupCriticalEventListeners');
|
||
const userId = window.currentDeleteUserId ||
|
||
(document.getElementById('deleteUserId') ? document.getElementById('deleteUserId').value : null);
|
||
directDeleteUserAPI(userId);
|
||
return false;
|
||
});
|
||
}
|
||
|
||
// Set up delete user form
|
||
const deleteUserForm = document.getElementById('deleteUserForm');
|
||
if (deleteUserForm) {
|
||
deleteUserForm.addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete user form submitted in setupCriticalEventListeners');
|
||
deleteUser();
|
||
return false;
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load user data from localStorage and API
|
||
*/
|
||
async function loadUserData() {
|
||
showLoading();
|
||
// Get the new display elements
|
||
const userNameDisplay = document.getElementById('currentUserNameDisplay');
|
||
const userEmailDisplay = document.getElementById('currentUserEmailDisplay');
|
||
|
||
try {
|
||
// Get user from localStorage first
|
||
const currentUser = window.auth.getCurrentUser();
|
||
|
||
if (currentUser) {
|
||
// Populate form fields
|
||
if (firstNameInput) firstNameInput.value = currentUser.first_name || ''; // Add null checks
|
||
if (lastNameInput) lastNameInput.value = currentUser.last_name || ''; // Add null checks
|
||
if (emailInput) emailInput.value = currentUser.email || ''; // Add null checks
|
||
|
||
// --- UPDATE DISPLAY ELEMENT (Initial Load) ---
|
||
let displayName;
|
||
if (currentUser.first_name && currentUser.last_name) {
|
||
displayName = `${currentUser.first_name} ${currentUser.last_name}`;
|
||
} else {
|
||
displayName = currentUser.username || 'User';
|
||
}
|
||
if (userNameDisplay) userNameDisplay.textContent = displayName;
|
||
if (userEmailDisplay) userEmailDisplay.textContent = currentUser.email || 'N/A';
|
||
// --- END UPDATE ---
|
||
|
||
// Check if user is admin and show admin section
|
||
if (currentUser.is_admin) {
|
||
if (adminSection) adminSection.style.display = 'block';
|
||
if (adminSection && adminSection.style.display === 'block') {
|
||
if (usersTableBody) loadUsers();
|
||
if (registrationEnabled) loadSiteSettings();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fetch fresh user data from API
|
||
try {
|
||
const response = await fetch('/api/auth/user', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const userData = await response.json();
|
||
|
||
// Update form fields with fresh data
|
||
if (firstNameInput) firstNameInput.value = userData.first_name || ''; // Add null checks
|
||
if (lastNameInput) lastNameInput.value = userData.last_name || ''; // Add null checks
|
||
if (emailInput) emailInput.value = userData.email || ''; // Add null checks
|
||
|
||
// --- UPDATE DISPLAY ELEMENT (After API Load) ---
|
||
let displayName;
|
||
if (userData.first_name && userData.last_name) {
|
||
displayName = `${userData.first_name} ${userData.last_name}`;
|
||
} else {
|
||
displayName = userData.username || 'User';
|
||
}
|
||
if (userNameDisplay) userNameDisplay.textContent = displayName;
|
||
if (userEmailDisplay) userEmailDisplay.textContent = userData.email || 'N/A';
|
||
// --- END UPDATE ---
|
||
|
||
// Show admin section if user is admin
|
||
if (userData.is_admin) {
|
||
if (adminSection) adminSection.style.display = 'block';
|
||
if (adminSection && adminSection.style.display === 'block') {
|
||
if (usersTableBody) loadUsers();
|
||
if (registrationEnabled) loadSiteSettings();
|
||
}
|
||
}
|
||
|
||
// Update localStorage ONLY if data has changed
|
||
const currentUser = window.auth.getCurrentUser();
|
||
let first_name = userData.first_name;
|
||
let last_name = userData.last_name;
|
||
if (!last_name) first_name = ''; // Reset first name if last name is empty
|
||
|
||
const updatedUser = {
|
||
...(currentUser || {}), // Preserve existing fields
|
||
first_name, // Update first name
|
||
last_name, // Update last name
|
||
// Preserve other essential fields from fetched data if currentUser was null
|
||
email: currentUser ? currentUser.email : userData.email,
|
||
username: currentUser ? currentUser.username : userData.username,
|
||
is_admin: currentUser ? currentUser.is_admin : userData.is_admin,
|
||
id: currentUser ? currentUser.id : userData.id
|
||
};
|
||
|
||
// Convert both to JSON strings for reliable comparison
|
||
const currentUserString = JSON.stringify(currentUser);
|
||
const updatedUserString = JSON.stringify(updatedUser);
|
||
|
||
if (currentUserString !== updatedUserString) {
|
||
console.log('User data changed, updating localStorage.');
|
||
localStorage.setItem('user_info', updatedUserString);
|
||
} else {
|
||
console.log('User data from API matches localStorage, skipping update.');
|
||
}
|
||
// localStorage.setItem('user_info', JSON.stringify(updatedUser)); // OLD LINE
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response' }));
|
||
console.warn('API error fetching user data:', errorData.message);
|
||
if (!currentUser) {
|
||
showToast(errorData.message || 'Failed to load fresh user data', 'warning');
|
||
}
|
||
}
|
||
} catch (apiError) {
|
||
console.warn('API error, using localStorage data:', apiError);
|
||
if (!currentUser) {
|
||
showToast('Could not connect to fetch user data. Displaying cached info.', 'warning');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading user data:', error);
|
||
showToast('Failed to load user data. Please try again.', 'error');
|
||
if (userNameDisplay) userNameDisplay.textContent = 'Error';
|
||
if (userEmailDisplay) userEmailDisplay.textContent = 'Error';
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get current user type (admin or user)
|
||
* @returns {string} 'admin' or 'user'
|
||
*/
|
||
function getUserType() {
|
||
try {
|
||
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
|
||
return userInfo.is_admin === true ? 'admin' : 'user';
|
||
} catch (e) {
|
||
console.error('Error determining user type:', e);
|
||
return 'user'; // Default to user if we can't determine
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the appropriate localStorage key prefix based on user type
|
||
* @returns {string} The prefix to use for localStorage keys
|
||
*/
|
||
function getPreferenceKeyPrefix() {
|
||
return getUserType() === 'admin' ? 'admin_' : 'user_';
|
||
}
|
||
|
||
/**
|
||
* Load user preferences
|
||
*/
|
||
async function loadPreferences() {
|
||
console.log('Loading preferences...');
|
||
const prefix = getPreferenceKeyPrefix();
|
||
console.log('Loading preferences with prefix:', prefix);
|
||
|
||
let darkModeFromAPI = null;
|
||
let apiPrefs = null;
|
||
// Try to load preferences from backend if authenticated
|
||
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
|
||
try {
|
||
const response = await fetch('/api/auth/preferences', {
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
});
|
||
if (response.ok) {
|
||
apiPrefs = await response.json();
|
||
if (apiPrefs && apiPrefs.theme) {
|
||
darkModeFromAPI = apiPrefs.theme === 'dark';
|
||
setTheme(darkModeFromAPI);
|
||
// Sync localStorage
|
||
localStorage.setItem('darkMode', darkModeFromAPI);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to load preferences from backend:', e);
|
||
}
|
||
}
|
||
// Fallback: use localStorage if not authenticated or API fails
|
||
if (darkModeFromAPI === null) {
|
||
const storedDarkMode = localStorage.getItem('darkMode') === 'true';
|
||
setTheme(storedDarkMode);
|
||
}
|
||
// --- Load Date Format --- Add this section
|
||
const storedDateFormat = localStorage.getItem('dateFormat');
|
||
if (storedDateFormat && dateFormatSelect) {
|
||
dateFormatSelect.value = storedDateFormat;
|
||
console.log(`Loaded dateFormat from localStorage: ${storedDateFormat}`);
|
||
} else if (dateFormatSelect) {
|
||
dateFormatSelect.value = 'MDY'; // Default if not found
|
||
console.log('dateFormat not found in localStorage, defaulting to MDY');
|
||
}
|
||
// --- End Date Format Section ---
|
||
|
||
// Default View
|
||
const storedView = localStorage.getItem(`${prefix}defaultView`);
|
||
if (storedView && defaultViewSelect) {
|
||
defaultViewSelect.value = storedView;
|
||
console.log(`Loaded default view from ${prefix}defaultView: ${storedView}`);
|
||
} else if (defaultViewSelect) {
|
||
defaultViewSelect.value = 'grid'; // Default
|
||
console.log(`${prefix}defaultView not found, defaulting view to grid`);
|
||
}
|
||
|
||
// Currency Symbol
|
||
const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`);
|
||
if (storedCurrency) {
|
||
if (currencySymbolSelect) {
|
||
// Check if the stored symbol is a standard option
|
||
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === storedCurrency);
|
||
if (standardOption) {
|
||
currencySymbolSelect.value = storedCurrency;
|
||
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
|
||
} else {
|
||
// It's a custom symbol
|
||
currencySymbolSelect.value = 'other';
|
||
if (currencySymbolCustom) {
|
||
currencySymbolCustom.value = storedCurrency;
|
||
currencySymbolCustom.style.display = 'inline-block';
|
||
}
|
||
}
|
||
console.log(`Loaded currency symbol from ${prefix}currencySymbol: ${storedCurrency}`);
|
||
}
|
||
} else {
|
||
// Default to '$' if nothing stored
|
||
if (currencySymbolSelect) currencySymbolSelect.value = '$';
|
||
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
|
||
console.log(`${prefix}currencySymbol not found, defaulting to $`);
|
||
}
|
||
|
||
// Expiring Soon Days
|
||
const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`);
|
||
if (storedExpiringDays && expiringSoonDaysInput) {
|
||
expiringSoonDaysInput.value = storedExpiringDays;
|
||
console.log(`Loaded expiring soon days from ${prefix}expiringSoonDays: ${storedExpiringDays}`);
|
||
} else if (expiringSoonDaysInput) {
|
||
expiringSoonDaysInput.value = 30; // Default
|
||
console.log(`${prefix}expiringSoonDays not found, defaulting to 30`);
|
||
}
|
||
|
||
// Now, try fetching preferences from API to override/confirm
|
||
if (window.auth && window.auth.isAuthenticated()) {
|
||
try {
|
||
const token = window.auth.getToken();
|
||
const response = await fetch('/api/auth/preferences', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const apiPrefs = await response.json();
|
||
console.log('Preferences loaded from API:', apiPrefs);
|
||
|
||
// Update UI elements with API data where available
|
||
if (apiPrefs.default_view && defaultViewSelect) {
|
||
// Only update if different from localStorage value (or if localStorage was empty)
|
||
const storedView = localStorage.getItem(`${prefix}defaultView`) || 'grid'; // Default if null
|
||
if (apiPrefs.default_view !== storedView) {
|
||
console.log(`API default_view (${apiPrefs.default_view}) differs from localStorage (${storedView}). Updating UI.`);
|
||
defaultViewSelect.value = apiPrefs.default_view;
|
||
}
|
||
}
|
||
// --- MODIFIED CURRENCY SYMBOL HANDLING ---
|
||
const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`); // Get localStorage value again for comparison
|
||
if (apiPrefs.currency_symbol && currencySymbolSelect) {
|
||
// Only update UI from API if the API value is different from what was in localStorage
|
||
// Or if localStorage didn't have a value initially
|
||
if (!storedCurrency || apiPrefs.currency_symbol !== storedCurrency) {
|
||
console.log(`API currency_symbol (${apiPrefs.currency_symbol}) differs from localStorage (${storedCurrency}). Updating UI.`);
|
||
// Logic to handle standard vs custom symbol from API
|
||
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === apiPrefs.currency_symbol);
|
||
if (standardOption) {
|
||
currencySymbolSelect.value = apiPrefs.currency_symbol;
|
||
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
|
||
} else {
|
||
currencySymbolSelect.value = 'other';
|
||
if (currencySymbolCustom) {
|
||
currencySymbolCustom.value = apiPrefs.currency_symbol;
|
||
currencySymbolCustom.style.display = 'inline-block';
|
||
}
|
||
}
|
||
} else {
|
||
console.log(`API currency_symbol (${apiPrefs.currency_symbol}) matches localStorage (${storedCurrency}). Skipping UI update.`);
|
||
}
|
||
}
|
||
// --- END MODIFIED CURRENCY SYMBOL HANDLING ---
|
||
if (apiPrefs.expiring_soon_days && expiringSoonDaysInput) {
|
||
// Only update if different from localStorage value (or if localStorage was empty)
|
||
const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`) || '30'; // Default if null
|
||
if (String(apiPrefs.expiring_soon_days) !== storedExpiringDays) {
|
||
console.log(`API expiring_soon_days (${apiPrefs.expiring_soon_days}) differs from localStorage (${storedExpiringDays}). Updating UI.`);
|
||
expiringSoonDaysInput.value = apiPrefs.expiring_soon_days;
|
||
}
|
||
}
|
||
|
||
// --- Update Date Format from API Prefs --- Add this check
|
||
const storedDateFormat = localStorage.getItem('dateFormat') || 'MDY'; // Default if null
|
||
if (apiPrefs.date_format && dateFormatSelect) {
|
||
if (apiPrefs.date_format !== storedDateFormat) {
|
||
console.log(`API date_format (${apiPrefs.date_format}) differs from localStorage (${storedDateFormat}). Updating UI.`);
|
||
dateFormatSelect.value = apiPrefs.date_format;
|
||
}
|
||
}
|
||
// --- End Date Format Check ---
|
||
|
||
// Update Email Settings from API
|
||
if (emailNotificationsToggle) {
|
||
emailNotificationsToggle.checked = apiPrefs.email_notifications !== false; // Default true if null/undefined
|
||
}
|
||
if (notificationFrequencySelect && apiPrefs.notification_frequency) {
|
||
notificationFrequencySelect.value = apiPrefs.notification_frequency;
|
||
}
|
||
if (notificationTimeInput && apiPrefs.notification_time) {
|
||
notificationTimeInput.value = apiPrefs.notification_time.substring(0, 5); // HH:MM format
|
||
}
|
||
// Load and set timezone from API
|
||
if (timezoneSelect && apiPrefs.timezone) {
|
||
console.log('API provided timezone:', apiPrefs.timezone);
|
||
// Ensure the option exists before setting
|
||
if (Array.from(timezoneSelect.options).some(option => option.value === apiPrefs.timezone)) {
|
||
timezoneSelect.value = apiPrefs.timezone;
|
||
console.log('Applied timezone from API:', timezoneSelect.value, 'Current select value:', timezoneSelect.value);
|
||
} else {
|
||
console.warn(`Timezone '${apiPrefs.timezone}' from API not found in dropdown.`);
|
||
}
|
||
} else {
|
||
console.log('No timezone preference found in API or timezone select element missing.');
|
||
}
|
||
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
console.warn(`Failed to load preferences from API: ${response.status}`, errorData.message || '');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching preferences from API:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Setup event listeners for the settings page
|
||
*/
|
||
function setupEventListeners() {
|
||
console.log('Setting up event listeners');
|
||
|
||
// Set up user menu button click handler
|
||
const userMenuBtn = document.getElementById('userMenuBtn');
|
||
const userMenuDropdown = document.getElementById('userMenuDropdown');
|
||
|
||
if (userMenuBtn && userMenuDropdown) {
|
||
console.log('Setting up user menu button click handler');
|
||
|
||
// Toggle dropdown when user button is clicked
|
||
userMenuBtn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
userMenuDropdown.classList.toggle('active');
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', function(e) {
|
||
if (userMenuDropdown.classList.contains('active') &&
|
||
!userMenuDropdown.contains(e.target) &&
|
||
!userMenuBtn.contains(e.target)) {
|
||
userMenuDropdown.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Dark mode toggle in header (no longer exists)
|
||
if (darkModeToggle) {
|
||
darkModeToggle.addEventListener('change', function() {
|
||
setTheme(this.checked);
|
||
|
||
// Also update the settings page toggle
|
||
if (darkModeToggleSetting) {
|
||
darkModeToggleSetting.checked = this.checked;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Dark mode toggle in settings
|
||
if (darkModeToggleSetting) {
|
||
darkModeToggleSetting.addEventListener('change', function() {
|
||
setTheme(this.checked);
|
||
|
||
// Also update the header toggle if it exists
|
||
if (darkModeToggle) {
|
||
darkModeToggle.checked = this.checked;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save profile button
|
||
if (saveProfileBtn) {
|
||
saveProfileBtn.addEventListener('click', saveProfile);
|
||
}
|
||
|
||
// Save preferences button
|
||
if (savePreferencesBtn) {
|
||
savePreferencesBtn.addEventListener('click', savePreferences);
|
||
}
|
||
|
||
// Change password button
|
||
if (changePasswordBtn) {
|
||
changePasswordBtn.addEventListener('click', function() {
|
||
passwordChangeForm.style.display = 'block';
|
||
this.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Save password button
|
||
if (savePasswordBtn) {
|
||
savePasswordBtn.addEventListener('click', changePassword);
|
||
}
|
||
|
||
// Cancel password button
|
||
if (cancelPasswordBtn) {
|
||
cancelPasswordBtn.addEventListener('click', function() {
|
||
resetPasswordForm();
|
||
passwordChangeForm.style.display = 'none';
|
||
changePasswordBtn.style.display = 'block';
|
||
});
|
||
}
|
||
|
||
// Delete account button
|
||
if (deleteAccountBtn) {
|
||
deleteAccountBtn.addEventListener('click', function() {
|
||
openModal(deleteAccountModal);
|
||
});
|
||
}
|
||
|
||
// Delete confirm input
|
||
if (deleteConfirmInput) {
|
||
deleteConfirmInput.addEventListener('input', function() {
|
||
confirmDeleteAccountBtn.disabled = this.value !== 'DELETE';
|
||
});
|
||
}
|
||
|
||
// Confirm delete account button
|
||
if (confirmDeleteAccountBtn) {
|
||
confirmDeleteAccountBtn.addEventListener('click', deleteAccount);
|
||
}
|
||
|
||
// Add event listener for logout menu item
|
||
const logoutMenuItem = document.getElementById('logoutMenuItem');
|
||
if (logoutMenuItem) {
|
||
logoutMenuItem.addEventListener('click', function() {
|
||
if (window.auth && window.auth.logout) {
|
||
window.auth.logout();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Admin section buttons
|
||
if (refreshUsersBtn) {
|
||
refreshUsersBtn.addEventListener('click', function() {
|
||
loadUsers();
|
||
});
|
||
}
|
||
|
||
if (checkAdminBtn) {
|
||
checkAdminBtn.addEventListener('click', function() {
|
||
checkAdminPermissions();
|
||
});
|
||
}
|
||
|
||
if (showUsersBtn) {
|
||
showUsersBtn.addEventListener('click', function() {
|
||
showUsersList();
|
||
});
|
||
}
|
||
|
||
if (testApiBtn) {
|
||
testApiBtn.addEventListener('click', function() {
|
||
checkApiEndpoint();
|
||
});
|
||
}
|
||
|
||
if (triggerNotificationsBtn) {
|
||
triggerNotificationsBtn.addEventListener('click', function() {
|
||
triggerWarrantyNotifications();
|
||
});
|
||
}
|
||
|
||
// Site settings save button
|
||
if (saveSiteSettingsBtn) {
|
||
saveSiteSettingsBtn.addEventListener('click', function() {
|
||
saveSiteSettings();
|
||
});
|
||
}
|
||
|
||
// Save email settings button
|
||
if (saveEmailSettingsBtn) {
|
||
saveEmailSettingsBtn.addEventListener('click', saveEmailSettings);
|
||
}
|
||
|
||
console.log('Event listeners setup complete');
|
||
}
|
||
|
||
/**
|
||
* Initialize modals
|
||
*/
|
||
function initModals() {
|
||
// Helper to close all modals and reset forms
|
||
function closeModalHandler(e) {
|
||
if (e) e.preventDefault();
|
||
document.querySelectorAll('.modal-backdrop').forEach(modal => {
|
||
modal.style.display = 'none';
|
||
});
|
||
// Reset delete confirm input
|
||
if (deleteConfirmInput) {
|
||
deleteConfirmInput.value = '';
|
||
confirmDeleteAccountBtn.disabled = true;
|
||
}
|
||
// Reset password form
|
||
resetPasswordForm();
|
||
}
|
||
|
||
// Close modal when clicking on X or outside
|
||
document.querySelectorAll('.close-btn, [data-dismiss="modal"]').forEach(closeBtn => {
|
||
closeBtn.addEventListener('click', closeModalHandler);
|
||
closeBtn.addEventListener('touchend', closeModalHandler);
|
||
});
|
||
|
||
// Close modal when clicking outside (backdrop)
|
||
function backdropHandler(event) {
|
||
document.querySelectorAll('.modal-backdrop').forEach(modal => {
|
||
if (event.target === modal) {
|
||
closeModalHandler(event);
|
||
}
|
||
});
|
||
}
|
||
window.addEventListener('click', backdropHandler);
|
||
window.addEventListener('touchend', backdropHandler);
|
||
|
||
// Add direct click handler to delete user modal
|
||
if (deleteUserModal) {
|
||
console.log('Adding click handler to deleteUserModal');
|
||
deleteUserModal.addEventListener('click', function(event) {
|
||
// Check if the click was on the confirm delete button
|
||
if (event.target && event.target.id === 'confirmDeleteUserBtn') {
|
||
event.preventDefault();
|
||
console.log('Confirm delete button clicked through modal event delegation');
|
||
deleteUser();
|
||
}
|
||
});
|
||
} else {
|
||
console.error('deleteUserModal not found in initModals');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open a modal
|
||
* @param {HTMLElement} modal - The modal to open
|
||
*/
|
||
function openModal(modal) {
|
||
console.log('Opening modal:', modal.id, 'Current display:', modal.style.display);
|
||
|
||
// First close all modals
|
||
closeAllModals();
|
||
|
||
// Then open this modal
|
||
modal.style.display = 'flex';
|
||
console.log('Modal display after opening:', modal.style.display);
|
||
|
||
// If this is the delete user modal, set up the delete button
|
||
if (modal.id === 'deleteUserModal') {
|
||
console.log('This is the delete user modal, setting up delete button');
|
||
// Use setTimeout to ensure the DOM is fully updated
|
||
setTimeout(() => {
|
||
setupDeleteButton();
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Reset password form
|
||
*/
|
||
function resetPasswordForm() {
|
||
currentPasswordInput.value = '';
|
||
newPasswordInput.value = '';
|
||
confirmPasswordInput.value = '';
|
||
}
|
||
|
||
/**
|
||
* Save user profile
|
||
*/
|
||
async function saveProfile() {
|
||
// Validate form
|
||
if (!firstNameInput || !lastNameInput || !firstNameInput.value.trim() || !lastNameInput.value.trim()) {
|
||
showToast('Please fill in First Name and Last Name', 'error');
|
||
return;
|
||
}
|
||
|
||
// Get the new email value
|
||
const newEmail = emailInput.value.trim();
|
||
if (!newEmail) {
|
||
showToast('Email address cannot be empty.', 'error');
|
||
return;
|
||
}
|
||
// Basic email format validation
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(newEmail)) {
|
||
showToast('Please enter a valid email address.', 'error');
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
const userNameDisplay = document.getElementById('currentUserNameDisplay');
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/profile', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
first_name: firstNameInput.value.trim(),
|
||
last_name: lastNameInput.value.trim(),
|
||
email: newEmail
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const userData = await response.json();
|
||
// Update localStorage
|
||
const currentUser = window.auth.getCurrentUser();
|
||
let first_name = userData.first_name;
|
||
let last_name = userData.last_name;
|
||
if (!last_name) first_name = '';
|
||
const updatedUser = {
|
||
...(currentUser || {}),
|
||
first_name,
|
||
last_name,
|
||
email: userData.email, // Use the email returned from the backend
|
||
username: currentUser ? currentUser.username : userData.username,
|
||
is_admin: currentUser ? currentUser.is_admin : userData.is_admin,
|
||
id: currentUser ? currentUser.id : userData.id
|
||
};
|
||
// Update the email input field with the (potentially new) email from the backend
|
||
if (emailInput) emailInput.value = userData.email || '';
|
||
localStorage.setItem('user_info', JSON.stringify(updatedUser));
|
||
|
||
// --- UPDATE DISPLAY ELEMENT IMMEDIATELY ---
|
||
let displayName;
|
||
if (userData.first_name && userData.last_name) {
|
||
displayName = `${userData.first_name} ${userData.last_name}`;
|
||
} else {
|
||
displayName = updatedUser.username || 'User';
|
||
}
|
||
if (userNameDisplay) userNameDisplay.textContent = displayName;
|
||
// --- END UPDATE ---
|
||
|
||
// Update UI (Header, etc.) - Ensure auth module is loaded
|
||
if (window.auth && window.auth.checkAuthState) {
|
||
window.auth.checkAuthState();
|
||
} else {
|
||
console.warn("Auth module or checkAuthState not found, header might not update immediately.");
|
||
}
|
||
|
||
showToast('Profile updated successfully', 'success');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response' }));
|
||
throw new Error(errorData.message || 'Failed to update profile');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating profile:', error);
|
||
showToast(`Failed to update profile: ${error.message}`, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Save user preferences
|
||
*/
|
||
async function savePreferences() {
|
||
console.log('Saving preferences...');
|
||
const prefix = getPreferenceKeyPrefix();
|
||
|
||
// --- Prepare data to save --- Add dateFormat and dark mode
|
||
const preferencesToSave = {
|
||
default_view: defaultViewSelect ? defaultViewSelect.value : 'grid',
|
||
expiring_soon_days: expiringSoonDaysInput ? parseInt(expiringSoonDaysInput.value) : 30,
|
||
date_format: dateFormatSelect ? dateFormatSelect.value : 'MDY',
|
||
theme: (localStorage.getItem('darkMode') === 'true') ? 'dark' : 'light',
|
||
};
|
||
|
||
// Handle currency symbol (standard or custom)
|
||
let currencySymbol = '$'; // Default
|
||
if (currencySymbolSelect) {
|
||
if (currencySymbolSelect.value === 'other' && currencySymbolCustom) {
|
||
currencySymbol = currencySymbolCustom.value.trim() || '$'; // Use custom or default to $ if empty
|
||
} else {
|
||
currencySymbol = currencySymbolSelect.value;
|
||
}
|
||
}
|
||
preferencesToSave.currency_symbol = currencySymbol;
|
||
// --- End data preparation ---
|
||
|
||
// +++ ADDED DEBUG LOGGING +++
|
||
console.log(`[SavePrefs Debug] Currency Select Value: ${currencySymbolSelect ? currencySymbolSelect.value : 'N/A'}`);
|
||
console.log(`[SavePrefs Debug] Custom Input Value: ${currencySymbolCustom ? currencySymbolCustom.value : 'N/A'}`);
|
||
console.log(`[SavePrefs Debug] Final currencySymbol value determined: ${currencySymbol}`);
|
||
// +++ END DEBUG LOGGING +++
|
||
|
||
// Save Dark Mode separately (using the single source of truth)
|
||
const isDark = darkModeToggleSetting ? darkModeToggleSetting.checked : false;
|
||
setTheme(isDark);
|
||
console.log(`Saved dark mode: ${isDark}`);
|
||
|
||
// Save simple preferences to localStorage immediately
|
||
localStorage.setItem('dateFormat', preferencesToSave.date_format); // Added
|
||
localStorage.setItem(`${prefix}defaultView`, preferencesToSave.default_view);
|
||
localStorage.setItem(`${prefix}currencySymbol`, preferencesToSave.currency_symbol);
|
||
localStorage.setItem(`${prefix}expiringSoonDays`, preferencesToSave.expiring_soon_days);
|
||
|
||
console.log('Preferences saved to localStorage (prefix:', prefix, '):', preferencesToSave);
|
||
console.log(`Value of dateFormat in localStorage: ${localStorage.getItem('dateFormat')}`);
|
||
|
||
// Try saving to API
|
||
if (window.auth && window.auth.isAuthenticated()) {
|
||
try {
|
||
showLoading();
|
||
const token = window.auth.getToken();
|
||
const response = await fetch('/api/auth/preferences', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(preferencesToSave)
|
||
});
|
||
|
||
hideLoading();
|
||
if (response.ok) {
|
||
showToast('Preferences saved successfully.', 'success');
|
||
console.log('Preferences successfully saved to API.');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.message || `Failed to save preferences to API: ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
hideLoading();
|
||
console.error('Error saving preferences to API:', error);
|
||
showToast(`Preferences saved locally, but failed to sync with server: ${error.message}`, 'warning');
|
||
}
|
||
} else {
|
||
// No auth, just show local save success
|
||
showToast('Preferences saved locally.', 'success');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Change user password
|
||
*/
|
||
async function changePassword() {
|
||
// Validate form
|
||
if (!currentPasswordInput.value || !newPasswordInput.value || !confirmPasswordInput.value) {
|
||
showToast('Please fill in all password fields', 'error');
|
||
return;
|
||
}
|
||
|
||
if (newPasswordInput.value !== confirmPasswordInput.value) {
|
||
showToast('New passwords do not match', 'error');
|
||
return;
|
||
}
|
||
|
||
// Validate password strength
|
||
if (newPasswordInput.value.length < 8) {
|
||
showToast('Password must be at least 8 characters long', 'error');
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
|
||
try {
|
||
try {
|
||
const response = await fetch('/api/auth/password/change', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
current_password: currentPasswordInput.value,
|
||
new_password: newPasswordInput.value
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Reset form and hide it
|
||
resetPasswordForm();
|
||
passwordChangeForm.style.display = 'none';
|
||
changePasswordBtn.style.display = 'block';
|
||
|
||
// Show success modal
|
||
openModal(passwordSuccessModal);
|
||
} else {
|
||
// Handle error
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || 'Failed to change password');
|
||
}
|
||
} catch (apiError) {
|
||
console.warn('API error, showing offline message:', apiError);
|
||
showToast('Password cannot be changed in offline mode', 'warning');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error changing password:', error);
|
||
showToast('Failed to change password. Please try again.', 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Delete user account
|
||
*/
|
||
async function deleteAccount() {
|
||
if (deleteConfirmInput.value !== 'DELETE') {
|
||
showToast('Please type DELETE to confirm', 'error');
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
|
||
try {
|
||
try {
|
||
const response = await fetch('/api/auth/account', {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Clear auth data
|
||
if (window.auth.logout) {
|
||
window.auth.logout();
|
||
} else {
|
||
localStorage.removeItem('auth_token');
|
||
localStorage.removeItem('user_info');
|
||
}
|
||
|
||
// Show success message
|
||
showToast('Account deleted successfully', 'success');
|
||
|
||
// Redirect to home page after a short delay
|
||
setTimeout(() => {
|
||
window.location.href = 'index.html';
|
||
}, 2000);
|
||
} else {
|
||
// Handle error
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || 'Failed to delete account');
|
||
}
|
||
} catch (apiError) {
|
||
console.warn('API error, showing offline message:', apiError);
|
||
showToast('Account cannot be deleted in offline mode', 'warning');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting account:', error);
|
||
showToast('Failed to delete account. Please try again.', 'error');
|
||
} finally {
|
||
hideLoading();
|
||
|
||
// Close modal
|
||
deleteAccountModal.style.display = 'none';
|
||
|
||
// Reset delete confirm input
|
||
deleteConfirmInput.value = '';
|
||
confirmDeleteAccountBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show loading spinner
|
||
*/
|
||
function showLoading() {
|
||
loadingContainer.style.display = 'flex';
|
||
}
|
||
|
||
/**
|
||
* Hide loading spinner
|
||
*/
|
||
function hideLoading() {
|
||
loadingContainer.style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* Show a toast notification
|
||
* @param {string} message - The message to display
|
||
* @param {string} type - The type of toast (info, success, error, warning)
|
||
* @param {number} duration - Duration in milliseconds, 0 for no auto-hide
|
||
* @returns {HTMLElement} - The toast element
|
||
*/
|
||
function showToast(message, type = 'info', duration = 5000) {
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast toast-${type}`;
|
||
|
||
const icon = document.createElement('i');
|
||
switch (type) {
|
||
case 'success':
|
||
icon.className = 'fas fa-check-circle';
|
||
break;
|
||
case 'error':
|
||
icon.className = 'fas fa-exclamation-circle';
|
||
break;
|
||
case 'warning':
|
||
icon.className = 'fas fa-exclamation-triangle';
|
||
break;
|
||
default:
|
||
icon.className = 'fas fa-info-circle';
|
||
}
|
||
|
||
const messageSpan = document.createElement('span');
|
||
messageSpan.textContent = message;
|
||
|
||
toast.appendChild(icon);
|
||
toast.appendChild(messageSpan);
|
||
|
||
toastContainer.appendChild(toast);
|
||
|
||
// Add a method to remove the toast
|
||
toast.remove = function() {
|
||
toast.classList.add('toast-fade-out');
|
||
setTimeout(() => {
|
||
if (toast.parentNode) {
|
||
toast.parentNode.removeChild(toast);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
// Auto-hide toast after specified duration (if not 0)
|
||
if (duration > 0) {
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, duration);
|
||
}
|
||
|
||
return toast;
|
||
}
|
||
|
||
/**
|
||
* Load users for admin
|
||
*/
|
||
async function loadUsers() {
|
||
// Exit if usersTableBody doesn't exist
|
||
if (!usersTableBody) {
|
||
console.log('usersTableBody element not found, skipping loadUsers');
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
|
||
try {
|
||
const response = await fetch('/api/admin/users', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const users = await response.json();
|
||
|
||
// Clear table
|
||
usersTableBody.innerHTML = '';
|
||
|
||
// Add users to table
|
||
users.forEach(user => {
|
||
const row = document.createElement('tr');
|
||
|
||
// Username
|
||
const usernameCell = document.createElement('td');
|
||
usernameCell.textContent = user.username;
|
||
row.appendChild(usernameCell);
|
||
|
||
// Email
|
||
const emailCell = document.createElement('td');
|
||
emailCell.textContent = user.email;
|
||
row.appendChild(emailCell);
|
||
|
||
// Name
|
||
const nameCell = document.createElement('td');
|
||
const fullName = `${user.first_name || ''} ${user.last_name || ''}`.trim();
|
||
nameCell.textContent = fullName || '-';
|
||
row.appendChild(nameCell);
|
||
|
||
// Status
|
||
const statusCell = document.createElement('td');
|
||
const statusBadge = document.createElement('span');
|
||
statusBadge.className = `badge ${user.is_active ? 'badge-success' : 'badge-danger'}`;
|
||
statusBadge.textContent = user.is_active ? 'Active' : 'Inactive';
|
||
statusCell.appendChild(statusBadge);
|
||
row.appendChild(statusCell);
|
||
|
||
// Admin
|
||
const adminCell = document.createElement('td');
|
||
const adminBadge = document.createElement('span');
|
||
adminBadge.className = `badge ${user.is_admin ? 'badge-primary' : 'badge-secondary'}`;
|
||
adminBadge.textContent = user.is_admin ? 'Admin' : 'User';
|
||
adminCell.appendChild(adminBadge);
|
||
row.appendChild(adminCell);
|
||
|
||
// Actions
|
||
const actionsCell = document.createElement('td');
|
||
|
||
// Edit button
|
||
const editBtn = document.createElement('button');
|
||
editBtn.className = 'btn btn-sm btn-outline-primary mr-2';
|
||
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
|
||
editBtn.title = 'Edit User';
|
||
editBtn.addEventListener('click', () => openEditUserModal(user));
|
||
|
||
// Delete button
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'btn btn-sm btn-outline-danger';
|
||
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
||
deleteBtn.title = 'Delete User';
|
||
|
||
// Add multiple event handlers for redundancy
|
||
deleteBtn.addEventListener('click', (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
console.log('Delete button clicked for user:', user);
|
||
openDeleteUserModal(user);
|
||
});
|
||
|
||
// Also set onclick property
|
||
deleteBtn.onclick = function(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
console.log('Delete button onclick property triggered for user:', user);
|
||
openDeleteUserModal(user);
|
||
return false;
|
||
};
|
||
|
||
// Don't allow editing or deleting self
|
||
const currentUser = window.auth.getCurrentUser();
|
||
if (user.id === currentUser.id) {
|
||
editBtn.disabled = true;
|
||
deleteBtn.disabled = true;
|
||
editBtn.title = 'Cannot edit yourself';
|
||
deleteBtn.title = 'Cannot delete yourself';
|
||
console.log('Disabled edit/delete for current user:', currentUser.id);
|
||
}
|
||
|
||
actionsCell.appendChild(editBtn);
|
||
actionsCell.appendChild(deleteBtn);
|
||
row.appendChild(actionsCell);
|
||
|
||
usersTableBody.appendChild(row);
|
||
});
|
||
|
||
console.log('Users loaded successfully:', users.length);
|
||
} else {
|
||
console.error('Failed to load users:', response.status);
|
||
showToast('Failed to load users', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading users:', error);
|
||
showToast('Error loading users', 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open edit user modal
|
||
* @param {Object} user - The user to edit
|
||
*/
|
||
function openEditUserModal(user) {
|
||
editUserId.value = user.id;
|
||
editUsername.value = user.username;
|
||
editEmail.value = user.email;
|
||
editUserActive.checked = user.is_active;
|
||
editUserAdmin.checked = user.is_admin;
|
||
|
||
openModal(editUserModal);
|
||
}
|
||
|
||
/**
|
||
* Save user changes
|
||
*/
|
||
async function saveUserChanges() {
|
||
const userId = editUserId.value;
|
||
|
||
if (!userId) {
|
||
showToast('User ID is missing', 'error');
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
|
||
try {
|
||
const response = await fetch(`/api/admin/users/${userId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
is_active: editUserActive.checked,
|
||
is_admin: editUserAdmin.checked
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('User updated successfully', 'success');
|
||
closeAllModals();
|
||
loadUsers();
|
||
} else {
|
||
const errorData = await response.json();
|
||
showToast(errorData.message || 'Failed to update user', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating user:', error);
|
||
showToast('Failed to update user. Please try again.', 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open delete user modal
|
||
* @param {Object} user - User object
|
||
*/
|
||
function openDeleteUserModal(user) {
|
||
console.log('Opening delete modal for user:', user);
|
||
|
||
// Store the user ID in a global variable for easier access
|
||
window.currentDeleteUserId = user.id;
|
||
console.log('Set global currentDeleteUserId to:', user.id);
|
||
|
||
// Set the user ID in the form
|
||
const deleteUserIdField = document.getElementById('deleteUserId');
|
||
if (deleteUserIdField) {
|
||
deleteUserIdField.value = user.id;
|
||
console.log('Set deleteUserId field to:', user.id);
|
||
} else {
|
||
console.error('deleteUserId field not found in the DOM');
|
||
}
|
||
|
||
// Display user ID for debugging
|
||
const displayUserIdField = document.getElementById('displayUserId');
|
||
if (displayUserIdField) {
|
||
displayUserIdField.textContent = user.id;
|
||
console.log('Set displayUserId field to:', user.id);
|
||
}
|
||
|
||
// Also set in editUserId for backward compatibility
|
||
if (editUserId) {
|
||
editUserId.value = user.id;
|
||
console.log('Set editUserId.value to:', user.id);
|
||
}
|
||
|
||
// Set the username in the modal
|
||
const deleteUserNameElement = document.getElementById('deleteUserName');
|
||
if (deleteUserNameElement) {
|
||
deleteUserNameElement.textContent = user.username;
|
||
console.log('Set deleteUserName to:', user.username);
|
||
} else {
|
||
console.error('deleteUserName element not found in the DOM');
|
||
}
|
||
|
||
// Make sure the modal is visible
|
||
const deleteUserModal = document.getElementById('deleteUserModal');
|
||
if (deleteUserModal) {
|
||
// First ensure all modals are closed
|
||
closeAllModals();
|
||
|
||
// Then open this modal
|
||
deleteUserModal.style.display = 'flex';
|
||
console.log('Delete user modal opened');
|
||
|
||
// Ensure the delete button has the correct click handler
|
||
const confirmDeleteUserBtn = document.getElementById('confirmDeleteUserBtn');
|
||
if (confirmDeleteUserBtn) {
|
||
// Remove existing event listeners by cloning
|
||
const newBtn = confirmDeleteUserBtn.cloneNode(true);
|
||
confirmDeleteUserBtn.parentNode.replaceChild(newBtn, confirmDeleteUserBtn);
|
||
|
||
// Add the click event listener
|
||
newBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked for user ID:', user.id);
|
||
deleteUser();
|
||
return false;
|
||
});
|
||
|
||
// Also set the direct onclick attribute as a simple function call
|
||
newBtn.setAttribute('onclick', 'console.log("Direct onclick attribute clicked"); deleteUser(); return false;');
|
||
console.log('Added click event listener to delete button');
|
||
|
||
// Add a direct click handler
|
||
newBtn.onclick = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Direct onclick property handler clicked for user ID:', user.id);
|
||
deleteUser();
|
||
return false;
|
||
};
|
||
} else {
|
||
console.error('confirmDeleteUserBtn not found in the DOM');
|
||
}
|
||
|
||
// Set up the direct delete link
|
||
const directDeleteLink = document.getElementById('directDeleteLink');
|
||
if (directDeleteLink) {
|
||
directDeleteLink.onclick = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Direct delete link clicked for user ID:', user.id);
|
||
deleteUser();
|
||
return false;
|
||
};
|
||
console.log('Added onclick handler to direct delete link');
|
||
}
|
||
|
||
// Set up the direct API link
|
||
const directAPILink = document.getElementById('directAPILink');
|
||
if (directAPILink) {
|
||
directAPILink.onclick = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Direct API link clicked for user ID:', user.id);
|
||
directDeleteUserAPI(user.id);
|
||
return false;
|
||
};
|
||
console.log('Added onclick handler to direct API link');
|
||
}
|
||
|
||
// Also set up form submit handler
|
||
const deleteUserForm = document.getElementById('deleteUserForm');
|
||
if (deleteUserForm) {
|
||
deleteUserForm.onsubmit = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete user form submitted for user ID:', user.id);
|
||
deleteUser();
|
||
return false;
|
||
};
|
||
console.log('Added onsubmit handler to delete user form');
|
||
}
|
||
|
||
// Add a direct click handler to the modal for event delegation
|
||
deleteUserModal.addEventListener('click', function(e) {
|
||
if (e.target && e.target.id === 'confirmDeleteUserBtn') {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked through modal event delegation for user ID:', user.id);
|
||
deleteUser();
|
||
return false;
|
||
}
|
||
});
|
||
} else {
|
||
console.error('deleteUserModal not found in the DOM');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Delete user
|
||
*/
|
||
function deleteUser() {
|
||
console.log('=== DELETE USER FUNCTION STARTED ===');
|
||
console.log('Function caller:', arguments.callee.caller ? arguments.callee.caller.name : 'unknown');
|
||
|
||
try {
|
||
// Get the user ID from various possible sources
|
||
const userId = window.currentDeleteUserId ||
|
||
(document.getElementById('deleteUserId') ? document.getElementById('deleteUserId').value : null) ||
|
||
(document.getElementById('editUserId') ? document.getElementById('editUserId').value : null) ||
|
||
(document.getElementById('displayUserId') ? document.getElementById('displayUserId').textContent : null);
|
||
|
||
console.log('Final User ID to delete:', userId);
|
||
|
||
if (!userId) {
|
||
showToast('User ID is missing', 'error');
|
||
console.error('Delete user failed: User ID is missing');
|
||
return;
|
||
}
|
||
|
||
console.log('Starting user deletion process for ID:', userId);
|
||
showLoading();
|
||
|
||
// Use our improved deletion function
|
||
superEmergencyDelete(userId)
|
||
.then(success => {
|
||
if (success) {
|
||
console.log('User deletion successful');
|
||
showToast('User deleted successfully', 'success');
|
||
closeAllModals();
|
||
loadUsers(); // Refresh the user list
|
||
|
||
// Refresh the users list if it's currently displayed
|
||
const usersModal = document.querySelector('div[style*="z-index: 10000"]');
|
||
if (usersModal) {
|
||
document.body.removeChild(usersModal);
|
||
showUsersList();
|
||
}
|
||
} else {
|
||
console.error('User deletion failed');
|
||
showToast('Failed to delete user. Check console for details.', 'error');
|
||
}
|
||
hideLoading();
|
||
})
|
||
.catch(error => {
|
||
console.error('Error during user deletion:', error);
|
||
showToast('Error during user deletion: ' + error.message, 'error');
|
||
hideLoading();
|
||
|
||
// Try the direct API call as a fallback
|
||
if (confirm('Would you like to try a direct API call to delete the user?')) {
|
||
const directToast = showToast(`Trying direct API call for user ID ${userId}...`, 'info', 0);
|
||
|
||
directDeleteUserAPI(userId)
|
||
.then(directSuccess => {
|
||
directToast.remove();
|
||
|
||
if (directSuccess) {
|
||
showToast(`User ID ${userId} deleted successfully with direct API call!`, 'success');
|
||
|
||
// Refresh the users list if it's currently displayed
|
||
const usersModal = document.querySelector('div[style*="z-index: 10000"]');
|
||
if (usersModal) {
|
||
document.body.removeChild(usersModal);
|
||
setTimeout(() => {
|
||
showUsersList();
|
||
}, 500);
|
||
}
|
||
} else {
|
||
showToast(`Failed to delete user ${user.username} with direct API call.`, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
directToast.remove();
|
||
console.error('Error with direct API call:', error);
|
||
showToast('Error with direct API call: ' + error.message, 'error');
|
||
});
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error checking if user exists:', error);
|
||
showToast('Error checking if user exists: ' + error.message, 'error');
|
||
});
|
||
} catch (error) {
|
||
console.error('Error in deleteUser function:', error);
|
||
console.error('Error details:', error.message, error.stack);
|
||
showToast('Failed to delete user. Please try again.', 'error');
|
||
hideLoading();
|
||
}
|
||
|
||
console.log('=== DELETE USER FUNCTION COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Super emergency delete function for user deletion
|
||
* @param {string|number} userId - The user ID or username to delete
|
||
* @returns {Promise<boolean>} - Promise that resolves to true if deletion was successful, false otherwise
|
||
*/
|
||
function superEmergencyDelete(userId) {
|
||
console.log('=== SUPER EMERGENCY DELETE STARTED ===');
|
||
console.log('User ID or username to delete:', userId);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
if (!userId) {
|
||
console.error('No user ID provided');
|
||
reject(new Error('No user ID provided'));
|
||
return;
|
||
}
|
||
|
||
// Check if the input is a username rather than a numeric ID
|
||
if (isNaN(userId)) {
|
||
console.log('Input appears to be a username, not a numeric ID');
|
||
// Try to find the user ID by username using the Promise-based function
|
||
findUserIdByUsernameAsync(userId)
|
||
.then(numericId => {
|
||
if (numericId) {
|
||
console.log(`Found numeric ID ${numericId} for username ${userId}`);
|
||
// Call this function again with the numeric ID
|
||
return superEmergencyDelete(numericId);
|
||
} else {
|
||
throw new Error(`Could not find a user with username "${userId}"`);
|
||
}
|
||
})
|
||
.then(resolve)
|
||
.catch(reject);
|
||
return;
|
||
}
|
||
|
||
console.log('Proceeding with numeric user ID:', userId);
|
||
|
||
// Get the token
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.error('Authentication token is missing');
|
||
reject(new Error('Authentication token is missing'));
|
||
return;
|
||
}
|
||
|
||
// Create a new XMLHttpRequest
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('DELETE', `/api/admin/users/${userId}`, true);
|
||
|
||
// Set headers
|
||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||
xhr.setRequestHeader('Accept', 'application/json');
|
||
|
||
// Set up event handlers
|
||
xhr.onload = function() {
|
||
console.log('XHR status:', xhr.status);
|
||
console.log('XHR response text:', xhr.responseText);
|
||
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
console.log('User deletion successful');
|
||
resolve(true);
|
||
} else {
|
||
console.error('User deletion failed with status:', xhr.status);
|
||
let errorMessage = 'Unknown error';
|
||
try {
|
||
const response = JSON.parse(xhr.responseText);
|
||
errorMessage = response.message || response.error || 'Unknown error';
|
||
} catch (e) {
|
||
errorMessage = xhr.responseText || 'Unknown error';
|
||
}
|
||
console.error('Error message:', errorMessage);
|
||
resolve(false); // Resolve with false instead of rejecting to handle the error in a controlled way
|
||
}
|
||
};
|
||
|
||
xhr.onerror = function() {
|
||
console.error('Network error during user deletion');
|
||
reject(new Error('Network error during user deletion'));
|
||
};
|
||
|
||
xhr.ontimeout = function() {
|
||
console.error('Request timeout during user deletion');
|
||
reject(new Error('Request timeout during user deletion'));
|
||
};
|
||
|
||
// Send the request
|
||
xhr.send();
|
||
console.log('Delete request sent for user ID:', userId);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Find a user's numeric ID by their username
|
||
* @param {string} username - The username to look up
|
||
* @param {function} callback - Callback function that receives the numeric ID or null if not found
|
||
*/
|
||
function findUserIdByUsername(username, callback) {
|
||
console.log('=== FIND USER ID BY USERNAME STARTED ===');
|
||
console.log('Looking up user ID for username:', username);
|
||
|
||
// Get the token
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.error('Authentication token is missing');
|
||
callback(null);
|
||
return;
|
||
}
|
||
|
||
// Fetch the list of users
|
||
fetch('/api/admin/users', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
console.error('Failed to fetch users list:', response.status);
|
||
callback(null);
|
||
return null;
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(users => {
|
||
if (!users) {
|
||
callback(null);
|
||
return;
|
||
}
|
||
|
||
console.log('Fetched users list:', users);
|
||
|
||
// Find the user with the matching username
|
||
const user = users.find(u => u.username === username);
|
||
|
||
if (user) {
|
||
console.log('Found user:', user);
|
||
callback(user.id);
|
||
} else {
|
||
console.error('User not found with username:', username);
|
||
callback(null);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching users:', error);
|
||
callback(null);
|
||
});
|
||
|
||
console.log('=== FIND USER ID BY USERNAME COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Check admin permissions
|
||
*/
|
||
function checkAdminPermissions() {
|
||
console.log('=== CHECK ADMIN PERMISSIONS STARTED ===');
|
||
|
||
// Get the token
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
alert('Error: Authentication token is missing');
|
||
return;
|
||
}
|
||
|
||
// Get the current user info
|
||
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
|
||
console.log('Current user info:', userInfo);
|
||
|
||
// Check if the user is an admin
|
||
if (userInfo.is_admin) {
|
||
console.log('User is an admin');
|
||
alert('You are an admin user');
|
||
} else {
|
||
console.log('User is not an admin');
|
||
alert('You are not an admin user');
|
||
}
|
||
|
||
// Make a request to the admin endpoint to verify permissions
|
||
fetch('/api/admin/users', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => {
|
||
console.log('Admin check response status:', response.status);
|
||
|
||
if (response.ok) {
|
||
console.log('Admin endpoint access successful');
|
||
alert('You have access to the admin endpoint');
|
||
} else {
|
||
console.error('Admin endpoint access failed');
|
||
alert('You do not have access to the admin endpoint');
|
||
}
|
||
|
||
return response.text().catch(() => '');
|
||
})
|
||
.then(text => {
|
||
console.log('Admin check response text:', text);
|
||
})
|
||
.catch(error => {
|
||
console.error('Admin check error:', error);
|
||
alert('Error checking admin permissions: ' + error.message);
|
||
});
|
||
|
||
console.log('=== CHECK ADMIN PERMISSIONS COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Show a list of users in the system
|
||
*/
|
||
function showUsersList() {
|
||
console.log('=== SHOW USERS LIST STARTED ===');
|
||
|
||
// Get the token
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
alert('Error: Authentication token is missing');
|
||
return;
|
||
}
|
||
|
||
// Fetch the list of users
|
||
fetch('/api/admin/users', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
console.error('Failed to fetch users list:', response.status);
|
||
alert('Failed to fetch users list. Status: ' + response.status);
|
||
return null;
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(users => {
|
||
if (!users) return;
|
||
|
||
console.log('Fetched users list:', users);
|
||
|
||
// Check if dark mode is enabled
|
||
const isDarkMode = document.body.classList.contains('dark-mode');
|
||
|
||
// Set colors based on theme
|
||
const backgroundColor = isDarkMode ? '#333' : 'white';
|
||
const textColor = isDarkMode ? '#fff' : '#000';
|
||
const borderColor = isDarkMode ? '#555' : '#ddd';
|
||
|
||
// Create a modal to display the users
|
||
const modal = document.createElement('div');
|
||
modal.style.position = 'fixed';
|
||
modal.style.top = '0';
|
||
modal.style.left = '0';
|
||
modal.style.width = '100%';
|
||
modal.style.height = '100%';
|
||
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||
modal.style.display = 'flex';
|
||
modal.style.justifyContent = 'center';
|
||
modal.style.alignItems = 'center';
|
||
modal.style.zIndex = '10000';
|
||
|
||
// Create the modal content
|
||
const modalContent = document.createElement('div');
|
||
modalContent.style.backgroundColor = backgroundColor;
|
||
modalContent.style.color = textColor;
|
||
modalContent.style.padding = '20px';
|
||
modalContent.style.borderRadius = '5px';
|
||
modalContent.style.maxWidth = '80%';
|
||
modalContent.style.maxHeight = '80%';
|
||
modalContent.style.overflow = 'auto';
|
||
|
||
// Create the modal header
|
||
const modalHeader = document.createElement('div');
|
||
modalHeader.style.display = 'flex';
|
||
modalHeader.style.justifyContent = 'space-between';
|
||
modalHeader.style.alignItems = 'center';
|
||
modalHeader.style.marginBottom = '20px';
|
||
|
||
// Create the modal title
|
||
const modalTitle = document.createElement('h3');
|
||
modalTitle.textContent = 'Users List';
|
||
modalTitle.style.color = textColor;
|
||
|
||
// Create the close button
|
||
const closeButton = document.createElement('button');
|
||
closeButton.textContent = '×';
|
||
closeButton.style.backgroundColor = 'transparent';
|
||
closeButton.style.border = 'none';
|
||
closeButton.style.fontSize = '24px';
|
||
closeButton.style.cursor = 'pointer';
|
||
closeButton.style.color = textColor;
|
||
closeButton.addEventListener('click', function() {
|
||
document.body.removeChild(modal);
|
||
});
|
||
|
||
// Add the title and close button to the header
|
||
modalHeader.appendChild(modalTitle);
|
||
modalHeader.appendChild(closeButton);
|
||
|
||
// Create the table
|
||
const table = document.createElement('table');
|
||
table.style.width = '100%';
|
||
table.style.borderCollapse = 'collapse';
|
||
|
||
// Create the table header
|
||
const thead = document.createElement('thead');
|
||
const headerRow = document.createElement('tr');
|
||
|
||
// Create the header cells
|
||
const headers = ['ID', 'Username', 'Email', 'Name', 'Admin', 'Active', 'Actions'];
|
||
headers.forEach(headerText => {
|
||
const th = document.createElement('th');
|
||
th.textContent = headerText;
|
||
th.style.padding = '10px';
|
||
th.style.textAlign = 'left';
|
||
th.style.borderBottom = `1px solid ${borderColor}`;
|
||
th.style.color = textColor;
|
||
headerRow.appendChild(th);
|
||
});
|
||
|
||
thead.appendChild(headerRow);
|
||
table.appendChild(thead);
|
||
|
||
// Create the table body
|
||
const tbody = document.createElement('tbody');
|
||
|
||
// Add a row for each user
|
||
users.forEach(user => {
|
||
const row = document.createElement('tr');
|
||
|
||
// Create cells for each property
|
||
const idCell = document.createElement('td');
|
||
idCell.textContent = user.id;
|
||
idCell.style.padding = '10px';
|
||
idCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
idCell.style.color = textColor;
|
||
|
||
const usernameCell = document.createElement('td');
|
||
usernameCell.textContent = user.username;
|
||
usernameCell.style.padding = '10px';
|
||
usernameCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
usernameCell.style.color = textColor;
|
||
|
||
const emailCell = document.createElement('td');
|
||
emailCell.textContent = user.email;
|
||
emailCell.style.padding = '10px';
|
||
emailCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
emailCell.style.color = textColor;
|
||
|
||
const nameCell = document.createElement('td');
|
||
nameCell.textContent = `${user.first_name || ''} ${user.last_name || ''}`.trim() || '-';
|
||
nameCell.style.padding = '10px';
|
||
nameCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
nameCell.style.color = textColor;
|
||
|
||
const adminCell = document.createElement('td');
|
||
adminCell.textContent = user.is_admin ? 'Yes' : 'No';
|
||
adminCell.style.padding = '10px';
|
||
adminCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
adminCell.style.color = textColor;
|
||
|
||
const activeCell = document.createElement('td');
|
||
activeCell.textContent = user.is_active ? 'Yes' : 'No';
|
||
activeCell.style.padding = '10px';
|
||
activeCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
activeCell.style.color = textColor;
|
||
|
||
const actionsCell = document.createElement('td');
|
||
actionsCell.style.padding = '10px';
|
||
actionsCell.style.borderBottom = `1px solid ${borderColor}`;
|
||
|
||
// Create a delete button
|
||
const deleteButton = document.createElement('button');
|
||
deleteButton.textContent = 'Delete';
|
||
deleteButton.style.backgroundColor = '#dc3545';
|
||
deleteButton.style.color = 'white';
|
||
deleteButton.style.border = 'none';
|
||
deleteButton.style.padding = '5px 10px';
|
||
deleteButton.style.borderRadius = '3px';
|
||
deleteButton.style.cursor = 'pointer';
|
||
|
||
// Don't allow deleting the current user
|
||
const currentUser = JSON.parse(localStorage.getItem('user_info') || '{}');
|
||
if (user.id === currentUser.id) {
|
||
deleteButton.disabled = true;
|
||
deleteButton.style.opacity = '0.5';
|
||
deleteButton.style.cursor = 'not-allowed';
|
||
deleteButton.title = 'Cannot delete yourself';
|
||
} else {
|
||
deleteButton.addEventListener('click', function() {
|
||
if (confirm(`Are you sure you want to delete user ${user.username} (ID: ${user.id})?`)) {
|
||
document.body.removeChild(modal);
|
||
|
||
// First check if the user still exists
|
||
const checkingToast = showToast(`Checking if user ${user.username} still exists...`, 'info', 0);
|
||
|
||
checkUserExists(user.id)
|
||
.then(existingUser => {
|
||
checkingToast.remove();
|
||
|
||
if (!existingUser) {
|
||
showToast(`User ${user.username} no longer exists`, 'warning');
|
||
showUsersList(); // Refresh the list
|
||
return;
|
||
}
|
||
|
||
// User exists, proceed with deletion
|
||
testUserDeletion(user.id);
|
||
})
|
||
.catch(error => {
|
||
checkingToast.remove();
|
||
console.error('Error checking if user exists:', error);
|
||
showToast('Error checking if user exists: ' + error.message, 'error');
|
||
|
||
// Ask if they want to try deletion anyway
|
||
if (confirm(`Error checking if user ${user.username} exists. Try deletion anyway?`)) {
|
||
testUserDeletion(user.id);
|
||
} else {
|
||
showUsersList(); // Refresh the list
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
actionsCell.appendChild(deleteButton);
|
||
|
||
// Add all cells to the row
|
||
row.appendChild(idCell);
|
||
row.appendChild(usernameCell);
|
||
row.appendChild(emailCell);
|
||
row.appendChild(nameCell);
|
||
row.appendChild(adminCell);
|
||
row.appendChild(activeCell);
|
||
row.appendChild(actionsCell);
|
||
|
||
// Add the row to the table body
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
table.appendChild(tbody);
|
||
|
||
// Add the header and table to the modal content
|
||
modalContent.appendChild(modalHeader);
|
||
modalContent.appendChild(table);
|
||
|
||
// Add the modal content to the modal
|
||
modal.appendChild(modalContent);
|
||
|
||
// Add the modal to the page
|
||
document.body.appendChild(modal);
|
||
|
||
console.log('Users list displayed');
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching users:', error);
|
||
alert('Error fetching users: ' + error.message);
|
||
});
|
||
|
||
console.log('=== SHOW USERS LIST COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Promise-based version of findUserIdByUsername
|
||
* @param {string} username - The username to look up
|
||
* @returns {Promise<number|null>} - Promise that resolves to the user ID or null if not found
|
||
*/
|
||
function findUserIdByUsernameAsync(username) {
|
||
return new Promise((resolve, reject) => {
|
||
findUserIdByUsername(username, (err, userId) => {
|
||
if (err) reject(err);
|
||
else resolve(userId);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Check if a user exists by ID or username
|
||
* @param {string|number} userIdOrUsername - The user ID or username to check
|
||
* @returns {Promise<Object|null>} - Promise that resolves to the user object if found, null otherwise
|
||
*/
|
||
function checkUserExists(userIdOrUsername) {
|
||
console.log('=== CHECK USER EXISTS STARTED ===');
|
||
console.log('Checking if user exists:', userIdOrUsername);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// Get the token
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.error('Authentication token is missing');
|
||
reject(new Error('Authentication token is missing'));
|
||
return;
|
||
}
|
||
|
||
// Fetch the list of users
|
||
fetch('/api/admin/users', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
console.error('Failed to fetch users list:', response.status);
|
||
reject(new Error(`Failed to fetch users list: ${response.status}`));
|
||
return null;
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(users => {
|
||
if (!users) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
console.log('Fetched users list for checking existence');
|
||
|
||
// Check if the input is a numeric ID
|
||
if (!isNaN(userIdOrUsername)) {
|
||
// Convert to number for comparison
|
||
const userId = Number(userIdOrUsername);
|
||
const user = users.find(u => u.id === userId);
|
||
|
||
if (user) {
|
||
console.log('Found user by ID:', user);
|
||
resolve(user);
|
||
} else {
|
||
console.log('User not found with ID:', userId);
|
||
resolve(null);
|
||
}
|
||
} else {
|
||
// Assume it's a username
|
||
const user = users.find(u => u.username === userIdOrUsername);
|
||
|
||
if (user) {
|
||
console.log('Found user by username:', user);
|
||
resolve(user);
|
||
} else {
|
||
console.log('User not found with username:', userIdOrUsername);
|
||
resolve(null);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error checking if user exists:', error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Test user deletion functionality
|
||
* @param {string|number} userId - The user ID or username to delete
|
||
*/
|
||
function testUserDeletion(userId) {
|
||
console.log('=== TEST USER DELETION STARTED ===');
|
||
console.log('Attempting to delete user:', userId);
|
||
|
||
// Check if we have a valid user ID
|
||
if (!userId) {
|
||
console.error('No user ID provided');
|
||
alert('Error: No user ID provided');
|
||
return;
|
||
}
|
||
|
||
// Show a loading indicator
|
||
const loadingToast = showToast('Checking user...', 'info', 0); // 0 means no auto-hide
|
||
|
||
// First check if the user exists
|
||
checkUserExists(userId)
|
||
.then(user => {
|
||
if (!user) {
|
||
loadingToast.remove();
|
||
showToast(`User with ID/username "${userId}" not found`, 'error');
|
||
return;
|
||
}
|
||
|
||
// Update the loading toast
|
||
loadingToast.remove();
|
||
const deletingToast = showToast(`Deleting user ${user.username} (ID: ${user.id})...`, 'info', 0);
|
||
|
||
// Use our improved deletion function with the numeric ID
|
||
return superEmergencyDelete(user.id)
|
||
.then(success => {
|
||
// Hide the loading toast
|
||
if (deletingToast) {
|
||
deletingToast.remove();
|
||
}
|
||
|
||
if (success) {
|
||
console.log('User deletion successful');
|
||
showToast(`User ${user.username} deleted successfully!`, 'success');
|
||
|
||
// Refresh the users list if it's currently displayed
|
||
const usersModal = document.querySelector('div[style*="z-index: 10000"]');
|
||
if (usersModal) {
|
||
document.body.removeChild(usersModal);
|
||
setTimeout(() => {
|
||
showUsersList();
|
||
}, 500);
|
||
}
|
||
} else {
|
||
console.error('User deletion failed');
|
||
showToast(`Failed to delete user ${user.username}. Check console for details.`, 'error');
|
||
|
||
// Offer to try direct API call
|
||
if (confirm(`Would you like to try a direct API call to delete user ${user.username}?`)) {
|
||
const directToast = showToast(`Trying direct API call for user ${user.username}...`, 'info', 0);
|
||
|
||
directDeleteUserAPI(user.id)
|
||
.then(directSuccess => {
|
||
directToast.remove();
|
||
|
||
if (directSuccess) {
|
||
showToast(`User ${user.username} deleted successfully with direct API call!`, 'success');
|
||
|
||
// Refresh the users list if it's currently displayed
|
||
const usersModal = document.querySelector('div[style*="z-index: 10000"]');
|
||
if (usersModal) {
|
||
document.body.removeChild(usersModal);
|
||
setTimeout(() => {
|
||
showUsersList();
|
||
}, 500);
|
||
}
|
||
} else {
|
||
showToast(`Failed to delete user ${user.username} with direct API call.`, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
directToast.remove();
|
||
console.error('Error with direct API call:', error);
|
||
showToast('Error with direct API call: ' + error.message, 'error');
|
||
});
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// Hide the loading toast
|
||
if (deletingToast) {
|
||
deletingToast.remove();
|
||
}
|
||
|
||
console.error('Error during user deletion:', error);
|
||
showToast('Error during user deletion: ' + error.message, 'error');
|
||
});
|
||
})
|
||
.catch(error => {
|
||
loadingToast.remove();
|
||
console.error('Error checking if user exists:', error);
|
||
showToast('Error checking if user exists: ' + error.message, 'error');
|
||
});
|
||
|
||
console.log('=== TEST USER DELETION COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Close all modals
|
||
*/
|
||
function closeAllModals() {
|
||
console.log('Closing all modals');
|
||
|
||
// Get all modals
|
||
const modals = document.querySelectorAll('.modal-backdrop');
|
||
|
||
// Close each modal
|
||
modals.forEach(modal => {
|
||
console.log('Closing modal:', modal.id);
|
||
modal.style.display = 'none';
|
||
});
|
||
|
||
// Also reset any form fields
|
||
if (deleteConfirmInput) {
|
||
deleteConfirmInput.value = '';
|
||
if (confirmDeleteAccountBtn) {
|
||
confirmDeleteAccountBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
// Reset password form
|
||
resetPasswordForm();
|
||
|
||
console.log('All modals closed');
|
||
}
|
||
|
||
/**
|
||
* Load site settings
|
||
*/
|
||
async function loadSiteSettings() {
|
||
console.log('Loading site settings...');
|
||
const adminSection = document.getElementById('adminSection');
|
||
const registrationToggle = document.getElementById('registrationEnabled');
|
||
const emailBaseUrlField = document.getElementById('emailBaseUrl'); // Correct variable name
|
||
|
||
// // Check if admin section exists
|
||
// if (!adminSection) {
|
||
// console.log('Admin section not found, skipping site settings load');
|
||
// return;
|
||
// }
|
||
|
||
try { // Correct structure: try block starts
|
||
showLoading();
|
||
|
||
const response = await fetch('/api/admin/settings', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to load site settings: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const settings = await response.json();
|
||
|
||
if (registrationToggle) {
|
||
registrationToggle.checked = settings.registration_enabled === 'true';
|
||
}
|
||
|
||
if (emailBaseUrlField) { // Use correct variable name
|
||
emailBaseUrlField.value = settings.email_base_url || 'http://localhost:8080'; // Set the value
|
||
}
|
||
|
||
console.log('Site settings loaded successfully', settings);
|
||
|
||
} catch (error) { // Correct structure: catch block follows try
|
||
console.error('Error loading site settings:', error);
|
||
showToast('Failed to load site settings. Please try again.', 'error');
|
||
} finally { // Correct structure: finally block follows catch
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Save site settings
|
||
*/
|
||
async function saveSiteSettings() {
|
||
console.log('Saving site settings...');
|
||
const registrationToggle = document.getElementById('registrationEnabled');
|
||
const emailBaseUrlField = document.getElementById('emailBaseUrl'); // Get the new field
|
||
|
||
const settings = {};
|
||
|
||
if (registrationToggle) {
|
||
settings.registration_enabled = registrationToggle.checked; // Boolean value will be converted to string in backend
|
||
}
|
||
|
||
if (emailBaseUrlField) {
|
||
let baseUrl = emailBaseUrlField.value.trim();
|
||
// Basic validation: check if it looks somewhat like a URL
|
||
if (baseUrl && (baseUrl.startsWith('http://') || baseUrl.startsWith('https://'))) {
|
||
// Remove trailing slash if present
|
||
if (baseUrl.endsWith('/')) {
|
||
baseUrl = baseUrl.slice(0, -1);
|
||
}
|
||
settings.email_base_url = baseUrl;
|
||
} else if (baseUrl) {
|
||
showToast('Invalid Email Base URL format. It should start with http:// or https://', 'error');
|
||
return; // Stop saving if format is invalid
|
||
} else {
|
||
settings.email_base_url = 'http://localhost:8080'; // Use default if empty
|
||
emailBaseUrlField.value = settings.email_base_url; // Update field with default
|
||
}
|
||
}
|
||
|
||
|
||
try {
|
||
showLoading();
|
||
|
||
const response = await fetch('/api/admin/settings', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(settings)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('Site settings saved successfully', 'success');
|
||
} else {
|
||
const errorData = await response.json();
|
||
showToast(errorData.message || 'Failed to save site settings', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving site settings:', error);
|
||
showToast('Failed to save site settings. Please try again.', 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Set up the delete button for user deletion
|
||
*/
|
||
function setupDeleteButton() {
|
||
console.log('=== SETUP DELETE BUTTON STARTED ===');
|
||
|
||
// Get the confirm delete button
|
||
const confirmDeleteUserBtn = document.getElementById('confirmDeleteUserBtn');
|
||
if (!confirmDeleteUserBtn) {
|
||
console.error('confirmDeleteUserBtn not found in setupDeleteButton');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Get the user ID from various possible sources
|
||
let userId = window.currentDeleteUserId || null;
|
||
|
||
if (!userId && document.getElementById('deleteUserId')) {
|
||
userId = document.getElementById('deleteUserId').value;
|
||
}
|
||
|
||
if (!userId && document.getElementById('editUserId')) {
|
||
userId = document.getElementById('editUserId').value;
|
||
}
|
||
|
||
if (!userId && document.getElementById('displayUserId')) {
|
||
userId = document.getElementById('displayUserId').textContent;
|
||
}
|
||
|
||
console.log('Setting up delete button for user ID:', userId);
|
||
|
||
// Remove existing event listeners by cloning
|
||
const newBtn = confirmDeleteUserBtn.cloneNode(true);
|
||
confirmDeleteUserBtn.parentNode.replaceChild(newBtn, confirmDeleteUserBtn);
|
||
|
||
// Add new event listener
|
||
newBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked in setupDeleteButton');
|
||
deleteUser();
|
||
return false;
|
||
});
|
||
|
||
// Also set onclick for redundancy
|
||
newBtn.onclick = function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Delete button clicked via onclick in setupDeleteButton');
|
||
deleteUser();
|
||
return false;
|
||
};
|
||
|
||
console.log('Delete button set up successfully');
|
||
} catch (error) {
|
||
console.error('Error setting up delete button:', error);
|
||
}
|
||
|
||
console.log('=== SETUP DELETE BUTTON COMPLETED ===');
|
||
}
|
||
|
||
/**
|
||
* Direct API call for user deletion
|
||
* @param {string|number} userId - The user ID or username to delete
|
||
* @returns {Promise<boolean>} - Promise that resolves to true if deletion was successful, false otherwise
|
||
*/
|
||
function directDeleteUserAPI(userId) {
|
||
console.log('=== DIRECT DELETE API CALL STARTED ===');
|
||
console.log('Function caller:', arguments.callee.caller ? arguments.callee.caller.name : 'unknown');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// Try to get the user ID from multiple sources if not provided
|
||
if (!userId) {
|
||
const deleteUserIdField = document.getElementById('deleteUserId');
|
||
const editUserIdField = document.getElementById('editUserId');
|
||
const displayUserIdField = document.getElementById('displayUserId');
|
||
|
||
userId = window.currentDeleteUserId;
|
||
|
||
if (!userId && deleteUserIdField && deleteUserIdField.value) {
|
||
userId = deleteUserIdField.value;
|
||
} else if (!userId && editUserIdField && editUserIdField.value) {
|
||
userId = editUserIdField.value;
|
||
} else if (!userId && displayUserIdField && displayUserIdField.textContent) {
|
||
userId = displayUserIdField.textContent;
|
||
}
|
||
}
|
||
|
||
console.log('User ID to delete:', userId);
|
||
|
||
if (!userId) {
|
||
console.error('Direct delete API call failed: User ID is missing');
|
||
reject(new Error('User ID is missing'));
|
||
return;
|
||
}
|
||
|
||
// Check if the input is a username rather than a numeric ID
|
||
if (isNaN(userId)) {
|
||
console.log('Input appears to be a username, not a numeric ID');
|
||
// Try to find the user ID by username
|
||
findUserIdByUsernameAsync(userId)
|
||
.then(numericId => {
|
||
if (numericId) {
|
||
console.log(`Found numeric ID ${numericId} for username ${userId}`);
|
||
// Call this function again with the numeric ID
|
||
return directDeleteUserAPI(numericId);
|
||
} else {
|
||
throw new Error(`Could not find a user with username "${userId}"`);
|
||
}
|
||
})
|
||
.then(resolve)
|
||
.catch(reject);
|
||
return;
|
||
}
|
||
|
||
console.log('Proceeding with numeric user ID:', userId);
|
||
|
||
// Get the token with detailed logging
|
||
const token = localStorage.getItem('auth_token');
|
||
console.log('Token exists:', !!token);
|
||
console.log('Token length:', token ? token.length : 0);
|
||
|
||
if (!token) {
|
||
console.error('Authentication token is missing');
|
||
reject(new Error('Authentication token is missing'));
|
||
return;
|
||
}
|
||
|
||
// Log the API endpoint
|
||
const apiEndpoint = `/api/admin/users/${userId}`;
|
||
console.log('Direct API endpoint:', apiEndpoint);
|
||
|
||
// Create headers with detailed logging
|
||
const headers = {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
};
|
||
console.log('Direct API request headers:', Object.keys(headers));
|
||
|
||
// Make the fetch request with detailed logging
|
||
console.log('Direct API fetch request configuration:', {
|
||
method: 'DELETE',
|
||
headers: Object.keys(headers),
|
||
credentials: 'same-origin'
|
||
});
|
||
|
||
// Make a fetch API call
|
||
fetch(apiEndpoint, {
|
||
method: 'DELETE',
|
||
headers: headers,
|
||
credentials: 'same-origin' // Include cookies
|
||
})
|
||
.then(response => {
|
||
console.log('Direct API response received');
|
||
console.log('Direct API response status:', response.status);
|
||
console.log('Direct API response status text:', response.statusText);
|
||
console.log('Direct API response headers:', [...response.headers.entries()]);
|
||
console.log('Direct API response type:', response.type);
|
||
console.log('Direct API response URL:', response.url);
|
||
|
||
// Get the raw text first
|
||
return response.text().then(text => {
|
||
console.log('Direct API raw response text:', text);
|
||
try {
|
||
const data = text ? JSON.parse(text) : {};
|
||
return {
|
||
status: response.status,
|
||
data: data
|
||
};
|
||
} catch (err) {
|
||
console.log('Error parsing Direct API JSON response:', err);
|
||
return {
|
||
status: response.status,
|
||
data: { message: text || 'No response data or invalid JSON' }
|
||
};
|
||
}
|
||
});
|
||
})
|
||
.then(result => {
|
||
console.log('Direct API response data:', result);
|
||
|
||
if (result.status >= 200 && result.status < 300) {
|
||
console.log('Direct API call successful');
|
||
resolve(true);
|
||
} else {
|
||
const errorMessage = result.data && result.data.message ? result.data.message : 'Failed to delete user';
|
||
console.error('Direct API call failed:', errorMessage);
|
||
resolve(false); // Resolve with false instead of rejecting
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Direct API call error:', error);
|
||
console.error('Direct API call error details:', error.message, error.stack);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Check if the API endpoint is accessible
|
||
*/
|
||
function checkApiEndpoint() {
|
||
console.log('Checking API endpoint accessibility...');
|
||
|
||
// Try to access a simple API endpoint
|
||
fetch('/api/auth/validate-token', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
})
|
||
.then(response => {
|
||
console.log('API endpoint check response status:', response.status);
|
||
if (response.ok) {
|
||
console.log('API endpoint is accessible');
|
||
showToast('API endpoint is accessible', 'success');
|
||
} else {
|
||
console.error('API endpoint is not accessible');
|
||
showToast('API endpoint is not accessible. Status: ' + response.status, 'error');
|
||
}
|
||
return response.json().catch(() => ({}));
|
||
})
|
||
.then(data => {
|
||
console.log('API endpoint check response data:', data);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error checking API endpoint:', error);
|
||
showToast('Error checking API endpoint: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Trigger warranty expiration notifications (admin only)
|
||
*/
|
||
async function triggerWarrantyNotifications() {
|
||
console.log('Trigger warranty notifications requested');
|
||
|
||
// Check if admin
|
||
try {
|
||
const response = await fetch('/api/auth/user', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to check user status');
|
||
}
|
||
|
||
const userData = await response.json();
|
||
|
||
if (!userData.is_admin) {
|
||
showToast('Only administrators can send warranty notifications', 'error');
|
||
return;
|
||
}
|
||
|
||
// Show confirmation dialog
|
||
if (!confirm('Are you sure you want to send warranty expiration notifications to all eligible users? This will immediately email users with warranties expiring soon.')) {
|
||
return;
|
||
}
|
||
|
||
showLoading();
|
||
|
||
// Call the API endpoint to trigger notifications
|
||
const notificationResponse = await fetch('/api/admin/send-notifications', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!notificationResponse.ok) {
|
||
const errorData = await notificationResponse.json();
|
||
throw new Error(errorData.message || 'Failed to send notifications');
|
||
}
|
||
|
||
const result = await notificationResponse.json();
|
||
showToast(result.message || 'Notifications triggered successfully', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error triggering notifications:', error);
|
||
showToast('Error: ' + error.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load available timezones from the API
|
||
* @returns {Promise} A promise that resolves when timezones are loaded
|
||
*/
|
||
function loadTimezones() {
|
||
console.log('Loading timezones...');
|
||
return new Promise((resolve, reject) => {
|
||
fetch('/api/timezones', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to load timezones');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(timezoneGroups => {
|
||
// Clear loading option
|
||
timezoneSelect.innerHTML = '';
|
||
|
||
// Add timezone groups and their options
|
||
timezoneGroups.forEach(group => {
|
||
const optgroup = document.createElement('optgroup');
|
||
optgroup.label = group.region;
|
||
|
||
group.timezones.forEach(timezone => {
|
||
const option = document.createElement('option');
|
||
option.value = timezone.value;
|
||
option.textContent = timezone.label;
|
||
optgroup.appendChild(option);
|
||
});
|
||
|
||
timezoneSelect.appendChild(optgroup);
|
||
});
|
||
|
||
// Set the current timezone from preferences
|
||
// Get the appropriate key prefix based on user type
|
||
const prefix = getPreferenceKeyPrefix();
|
||
const savedPreferences = JSON.parse(localStorage.getItem(`${prefix}preferences`) || '{}');
|
||
|
||
console.log('Loading timezone preference from localStorage', {
|
||
prefix: prefix,
|
||
preferenceKey: `${prefix}preferences`,
|
||
savedTimezone: savedPreferences.timezone
|
||
});
|
||
|
||
if (savedPreferences.timezone) {
|
||
timezoneSelect.value = savedPreferences.timezone;
|
||
console.log('Set timezone select to:', savedPreferences.timezone, 'Current value:', timezoneSelect.value);
|
||
resolve();
|
||
} else {
|
||
// If no timezone preference found in localStorage, load from API as backup
|
||
console.log('No timezone found in localStorage, fetching from API');
|
||
fetch('/api/auth/preferences', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// API returns preferences directly, not nested
|
||
if (data && data.timezone) {
|
||
console.log('Received timezone from API:', data.timezone);
|
||
timezoneSelect.value = data.timezone;
|
||
console.log('Set timezone select to:', data.timezone, 'Current value:', timezoneSelect.value);
|
||
}
|
||
resolve();
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading preferences from API:', error);
|
||
resolve(); // Still resolve to continue the chain
|
||
});
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading timezones:', error);
|
||
timezoneSelect.innerHTML = '<option value="UTC">UTC (Coordinated Universal Time)</option>';
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Save email settings
|
||
*/
|
||
function saveEmailSettings() {
|
||
showLoading();
|
||
|
||
try {
|
||
// Get values
|
||
const emailNotifications = emailNotificationsToggle.checked;
|
||
const notificationFrequency = notificationFrequencySelect.value;
|
||
const notificationTime = notificationTimeInput.value;
|
||
const timezone = timezoneSelect.value;
|
||
|
||
// Validate inputs
|
||
if (!timezone) {
|
||
showToast('Please select a timezone', 'error');
|
||
hideLoading();
|
||
return;
|
||
}
|
||
|
||
// Create preferences object
|
||
const preferences = {
|
||
email_notifications: emailNotifications,
|
||
notification_frequency: notificationFrequency,
|
||
notification_time: notificationTime,
|
||
timezone: timezone
|
||
};
|
||
|
||
// Save to API
|
||
fetch('/api/auth/preferences', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(preferences)
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to save email settings');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
// Save to localStorage
|
||
const prefix = getPreferenceKeyPrefix();
|
||
localStorage.setItem(`${prefix}emailNotifications`, emailNotifications);
|
||
localStorage.setItem(`${prefix}notificationFrequency`, notificationFrequency);
|
||
localStorage.setItem(`${prefix}notificationTime`, notificationTime);
|
||
localStorage.setItem(`${prefix}timezone`, timezone);
|
||
|
||
showToast('Email settings saved successfully', 'success');
|
||
})
|
||
.catch(error => {
|
||
console.error('Error saving email settings:', error);
|
||
showToast('Error saving email settings', 'error');
|
||
|
||
// Save to localStorage as fallback
|
||
const prefix = getPreferenceKeyPrefix();
|
||
localStorage.setItem(`${prefix}emailNotifications`, emailNotifications);
|
||
localStorage.setItem(`${prefix}notificationFrequency`, notificationFrequency);
|
||
localStorage.setItem(`${prefix}notificationTime`, notificationTime);
|
||
localStorage.setItem(`${prefix}timezone`, timezone);
|
||
})
|
||
.finally(() => {
|
||
hideLoading();
|
||
});
|
||
} catch (error) {
|
||
console.error('Error in saveEmailSettings:', error);
|
||
showToast('Error saving email settings', 'error');
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
// --- Add Storage Event Listener for Real-time Sync ---
|
||
window.addEventListener('storage', (event) => {
|
||
console.log(`[Settings Storage Listener] Event received: key=${event.key}, newValue=${event.newValue}`); // Log all events
|
||
|
||
const prefix = getPreferenceKeyPrefix();
|
||
const targetKey = `${prefix}defaultView`;
|
||
|
||
// Only react to changes in the specific default view key for the current user type
|
||
if (event.key === targetKey) { // Check key match first
|
||
console.log(`[Settings Storage Listener] Matched key: ${targetKey}`);
|
||
console.log(`[Settings Storage Listener] defaultViewSelect exists: ${!!defaultViewSelect}`);
|
||
if (defaultViewSelect) {
|
||
console.log(`[Settings Storage Listener] Current dropdown value: ${defaultViewSelect.value}`);
|
||
}
|
||
console.log(`[Settings Storage Listener] Event newValue: ${event.newValue}`);
|
||
|
||
if (event.newValue && defaultViewSelect && defaultViewSelect.value !== event.newValue) {
|
||
console.log(`[Settings Storage Listener] Value changed and dropdown exists. Checking options...`);
|
||
const optionExists = [...defaultViewSelect.options].some(option => option.value === event.newValue);
|
||
console.log(`[Settings Storage Listener] Option ${event.newValue} exists: ${optionExists}`);
|
||
if (optionExists) {
|
||
defaultViewSelect.value = event.newValue;
|
||
console.log(`[Settings Storage Listener] SUCCESS: Updated settings default view dropdown via storage event to ${event.newValue}.`);
|
||
} else {
|
||
console.warn(`[Settings Storage Listener] Storage event value (${event.newValue}) not found in dropdown options.`);
|
||
}
|
||
} else if (!event.newValue) {
|
||
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because newValue is null/empty.`);
|
||
} else if (!defaultViewSelect) {
|
||
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because defaultViewSelect element not found.`);
|
||
} else {
|
||
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because value hasn't changed (${defaultViewSelect.value} === ${event.newValue}).`);
|
||
}
|
||
}
|
||
|
||
// Add similar checks for other preferences if needed, e.g., dateFormat, currencySymbol
|
||
if (event.key === 'dateFormat') { // Simplified log for other keys
|
||
console.log(`[Settings Storage Listener] dateFormat changed to ${event.newValue}`);
|
||
if (event.newValue && dateFormatSelect && dateFormatSelect.value !== event.newValue) {
|
||
const optionExists = [...dateFormatSelect.options].some(option => option.value === event.newValue);
|
||
if (optionExists) {
|
||
dateFormatSelect.value = event.newValue;
|
||
console.log('[Settings Storage Listener] Updated settings date format dropdown via storage event.');
|
||
}
|
||
}
|
||
}
|
||
|
||
if (event.key === `${prefix}currencySymbol`) { // Simplified log for other keys
|
||
console.log(`[Settings Storage Listener] ${prefix}currencySymbol changed to ${event.newValue}`);
|
||
if (event.newValue && currencySymbolSelect && currencySymbolSelect.value !== event.newValue) {
|
||
// Handle standard vs custom symbol update
|
||
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === event.newValue);
|
||
if (standardOption) {
|
||
currencySymbolSelect.value = event.newValue;
|
||
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
|
||
console.log('[Settings Storage Listener] Updated settings currency dropdown via storage event.');
|
||
} else if (currencySymbolSelect.value !== 'other' || (currencySymbolCustom && currencySymbolCustom.value !== event.newValue)) {
|
||
currencySymbolSelect.value = 'other';
|
||
if (currencySymbolCustom) {
|
||
currencySymbolCustom.value = event.newValue;
|
||
currencySymbolCustom.style.display = 'inline-block';
|
||
}
|
||
console.log('[Settings Storage Listener] Updated settings currency dropdown to custom via storage event.');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add check for expiringSoonDays
|
||
if (event.key === `${prefix}expiringSoonDays`) { // Simplified log for other keys
|
||
console.log(`[Settings Storage Listener] ${prefix}expiringSoonDays changed to ${event.newValue}`);
|
||
if (event.newValue && expiringSoonDaysInput && expiringSoonDaysInput.value !== event.newValue) {
|
||
expiringSoonDaysInput.value = event.newValue;
|
||
console.log('[Settings Storage Listener] Updated settings expiring soon days input via storage event.');
|
||
}
|
||
}
|
||
|
||
});
|
||
// --- End Storage Event Listener ---
|
||
|
||
// Add event listener for dropdown to show/hide custom input
|
||
if (currencySymbolSelect && currencySymbolCustom) {
|
||
currencySymbolSelect.addEventListener('change', function() {
|
||
if (this.value === 'other') {
|
||
currencySymbolCustom.style.display = '';
|
||
currencySymbolCustom.focus();
|
||
} else {
|
||
currencySymbolCustom.style.display = 'none';
|
||
currencySymbolCustom.value = '';
|
||
}
|
||
});
|
||
} |