/** * Main Admin Panel Coordinator * This refactored admin.js coordinates all admin modules while maintaining functionality * Modules: core, auth, map, walksheet, shifts, shift-volunteers, users, email, integration */ /** * Main Event Listener Setup * Coordinates event listeners across all modules */ function setupAllEventListeners() { // Setup authentication listeners if (window.adminAuth && typeof window.adminAuth.setupAuthEventListeners === 'function') { window.adminAuth.setupAuthEventListeners(); } // Setup map listeners if (window.adminMap && typeof window.adminMap.setupMapEventListeners === 'function') { window.adminMap.setupMapEventListeners(); } // Setup walk sheet listeners if (window.adminWalkSheet && typeof window.adminWalkSheet.setupWalkSheetEventListeners === 'function') { window.adminWalkSheet.setupWalkSheetEventListeners(); } // Setup shift listeners if (window.adminShifts && typeof window.adminShifts.setupShiftEventListeners === 'function') { window.adminShifts.setupShiftEventListeners(); } // Setup shift volunteer listeners if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.setupShiftVolunteerEventListeners === 'function') { window.adminShiftVolunteers.setupShiftVolunteerEventListeners(); } // Setup user listeners if (window.adminUsers && typeof window.adminUsers.setupUserEventListeners === 'function') { window.adminUsers.setupUserEventListeners(); } // Setup email listeners if (window.adminEmail && typeof window.adminEmail.setupEmailEventListeners === 'function') { window.adminEmail.setupEmailEventListeners(); } } // Main admin initialization when DOM is loaded document.addEventListener('DOMContentLoaded', () => { // Initialize core functionality if (window.adminCore && typeof window.adminCore.initializeAdminCore === 'function') { window.adminCore.initializeAdminCore(); } // Initialize authentication if (window.adminAuth && typeof window.adminAuth.checkAdminAuth === 'function') { window.adminAuth.checkAdminAuth(); } // Initialize admin map if (window.adminMap && typeof window.adminMap.initializeAdminMap === 'function') { window.adminMap.initializeAdminMap(); } // Load current start location if (window.adminMap && typeof window.adminMap.loadCurrentStartLocation === 'function') { window.adminMap.loadCurrentStartLocation(); } // Setup all event listeners setupAllEventListeners(); // Initialize integrations with a small delay to ensure DOM is ready setTimeout(() => { if (window.adminWalkSheet && typeof window.adminWalkSheet.loadWalkSheetConfig === 'function') { window.adminWalkSheet.loadWalkSheetConfig(); } if (window.adminIntegration && typeof window.adminIntegration.initializeAllIntegrations === 'function') { window.adminIntegration.initializeAllIntegrations(); } }, 100); // Check if URL has a hash to show specific section const hash = window.location.hash; if (hash === '#walk-sheet') { if (window.adminCore && typeof window.adminCore.showSection === 'function') { window.adminCore.showSection('walk-sheet'); } if (window.adminWalkSheet && typeof window.adminWalkSheet.checkAndLoadWalkSheetConfig === 'function') { window.adminWalkSheet.checkAndLoadWalkSheetConfig(); } } else if (hash === '#convert-data') { if (window.adminCore && typeof window.adminCore.showSection === 'function') { window.adminCore.showSection('convert-data'); } } else if (hash === '#cuts') { if (window.adminCore && typeof window.adminCore.showSection === 'function') { window.adminCore.showSection('cuts'); } } else { // Default to dashboard if (window.adminCore && typeof window.adminCore.showSection === 'function') { window.adminCore.showSection('dashboard'); } // Load dashboard data on initial page load if (typeof loadDashboardData === 'function') { loadDashboardData(); } } }); /** * Dashboard Functions * Loads dashboard data and displays summary statistics */ async function loadDashboardData() { try { const response = await fetch('/api/admin/dashboard'); const data = await response.json(); if (data.success) { document.getElementById('total-users').textContent = data.stats.totalUsers; document.getElementById('total-shifts').textContent = data.stats.totalShifts; document.getElementById('total-signups').textContent = data.stats.totalSignups; document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers; } } catch (error) { console.error('Failed to load dashboard data:', error); } } /** * Legacy function redirects for backward compatibility * These ensure existing functionality continues to work */ window.loadDashboardData = loadDashboardData; // Export dashboard function for module coordination if (typeof window.adminDashboard === 'undefined') { window.adminDashboard = { loadDashboardData: loadDashboardData }; } // Add shift management functions async function loadAdminShifts() { const list = document.getElementById('admin-shifts-list'); if (list) { list.innerHTML = '

Loading shifts...

'; } try { console.log('Loading admin shifts...'); const response = await fetch('/api/shifts/admin'); const data = await response.json(); if (data.success) { console.log('Successfully loaded', data.shifts.length, 'shifts'); displayAdminShifts(data.shifts); } else { console.error('Failed to load shifts:', data.error); if (list) { list.innerHTML = '

Failed to load shifts

'; } showStatus('Failed to load shifts', 'error'); } } catch (error) { console.error('Error loading admin shifts:', error); if (list) { list.innerHTML = '

Error loading shifts

'; } showStatus('Failed to load shifts', 'error'); } } function displayAdminShifts(shifts) { const list = document.getElementById('admin-shifts-list'); if (!list) { console.error('Admin shifts list element not found'); return; } if (shifts.length === 0) { list.innerHTML = '

No shifts created yet.

'; return; } list.innerHTML = shifts.map(shift => { const shiftDate = createLocalDate(shift.Date); const signupCount = shift.signups ? shift.signups.length : 0; const isPublic = shift['Is Public'] !== false; console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []); return `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${signupCount}/${shift['Max Volunteers']} volunteers

${shift.Status || 'Open'}

${isPublic ? '🌐 Public' : '🔒 Private'}

${isPublic ? ` ` : ''}
`; }).join(''); // Add event listeners using delegation setupShiftActionListeners(); } // Fix the setupShiftActionListeners function function setupShiftActionListeners() { const list = document.getElementById('admin-shifts-list'); if (!list) return; // Remove any existing listeners to avoid duplicates const newList = list.cloneNode(true); list.parentNode.replaceChild(newList, list); // Get the updated reference const updatedList = document.getElementById('admin-shifts-list'); updatedList.addEventListener('click', function(e) { if (e.target.classList.contains('delete-shift-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); console.log('Delete button clicked for shift:', shiftId); deleteShift(shiftId); } else if (e.target.classList.contains('edit-shift-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); console.log('Edit button clicked for shift:', shiftId); editShift(shiftId); } else if (e.target.classList.contains('manage-volunteers-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/'/g, "'")); console.log('Manage volunteers clicked for shift:', shiftId); showShiftUserModal(shiftId, shiftData); } else if (e.target.classList.contains('copy-shift-link-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); console.log('Copy link button clicked for shift:', shiftId); copyShiftLink(shiftId); } else if (e.target.classList.contains('open-shift-link-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); console.log('Open link button clicked for shift:', shiftId); openShiftLink(shiftId); } }); } // Update the deleteShift function (remove window. prefix) async function deleteShift(shiftId) { if (!confirm('Are you sure you want to delete this shift? All signups will be cancelled.')) { return; } try { const response = await fetch(`/api/shifts/admin/${shiftId}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { showStatus('Shift deleted successfully', 'success'); await loadAdminShifts(); console.log('Refreshed shifts list after deleting shift'); } else { showStatus(data.error || 'Failed to delete shift', 'error'); } } catch (error) { console.error('Error deleting shift:', error); showStatus('Failed to delete shift', 'error'); } } // Update editShift function (remove window. prefix) async function editShift(shiftId) { try { // Find the shift in the current data const response = await fetch('/api/shifts/admin'); const data = await response.json(); if (!data.success) { showStatus('Failed to load shift data', 'error'); return; } const shift = data.shifts.find(s => s.ID === parseInt(shiftId)); if (!shift) { showStatus('Shift not found', 'error'); return; } // Set editing mode editingShiftId = shiftId; // Populate the form document.getElementById('shift-title').value = shift.Title || ''; document.getElementById('shift-description').value = shift.Description || ''; document.getElementById('shift-date').value = shift.Date || ''; document.getElementById('shift-start').value = shift['Start Time'] || ''; document.getElementById('shift-end').value = shift['End Time'] || ''; document.getElementById('shift-location').value = shift.Location || ''; document.getElementById('shift-max-volunteers').value = shift['Max Volunteers'] || ''; // Update public checkbox if it exists const publicCheckbox = document.getElementById('shift-is-public'); if (publicCheckbox) { publicCheckbox.checked = shift['Is Public'] !== false; } // Change submit button text const submitBtn = document.querySelector('#shift-form button[type="submit"]'); if (submitBtn) { submitBtn.textContent = 'Update Shift'; } // Remove editing class from any previous item document.querySelectorAll('.shift-admin-item.editing').forEach(el => { el.classList.remove('editing'); }); // Add editing class to current item const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`); if (shiftElement) { const shiftItem = shiftElement.closest('.shift-admin-item'); if (shiftItem) { shiftItem.classList.add('editing'); } } // Scroll to form document.getElementById('shift-form').scrollIntoView({ behavior: 'smooth' }); showStatus('Editing shift: ' + shift.Title, 'info'); } catch (error) { console.error('Error loading shift for edit:', error); showStatus('Failed to load shift for editing', 'error'); } } // Add function to create shift async function createShift(e) { e.preventDefault(); const title = document.getElementById('shift-title').value; const description = document.getElementById('shift-description').value; const date = document.getElementById('shift-date').value; const startTime = document.getElementById('shift-start').value; const endTime = document.getElementById('shift-end').value; const location = document.getElementById('shift-location').value; const maxVolunteers = document.getElementById('shift-max-volunteers').value; // Get public checkbox value const isPublic = document.getElementById('shift-is-public')?.checked ?? true; const shiftData = { title, description, date, startTime, endTime, location, maxVolunteers: parseInt(maxVolunteers), isPublic }; try { let response; if (editingShiftId) { // Update existing shift response = await fetch(`/api/shifts/admin/${editingShiftId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(shiftData) }); } else { // Create new shift response = await fetch('/api/shifts/admin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(shiftData) }); } const data = await response.json(); if (data.success) { showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success'); clearShiftForm(); await loadAdminShifts(); console.log('Refreshed shifts list after saving shift'); } else { showStatus(data.error || 'Failed to save shift', 'error'); } } catch (error) { console.error('Error saving shift:', error); showStatus('Failed to save shift', 'error'); } } function clearShiftForm() { const form = document.getElementById('shift-form'); if (form) { form.reset(); // Reset editing state editingShiftId = null; // Reset submit button text const submitBtn = document.querySelector('#shift-form button[type="submit"]'); if (submitBtn) { submitBtn.textContent = 'Create Shift'; } // Remove editing class from any shift items document.querySelectorAll('.shift-admin-item.editing').forEach(el => { el.classList.remove('editing'); }); showStatus('Form cleared', 'info'); } } // User Management Functions async function loadUsers() { const loadingEl = document.getElementById('users-loading'); const emptyEl = document.getElementById('users-empty'); const tableBody = document.getElementById('users-table-body'); if (loadingEl) loadingEl.style.display = 'block'; if (emptyEl) emptyEl.style.display = 'none'; if (tableBody) tableBody.innerHTML = ''; try { const response = await fetch('/api/users'); const data = await response.json(); if (loadingEl) loadingEl.style.display = 'none'; if (data.success && data.users) { displayUsers(data.users); } else { throw new Error(data.error || 'Failed to load users'); } } catch (error) { console.error('Error loading users:', error); if (loadingEl) loadingEl.style.display = 'none'; if (emptyEl) { emptyEl.textContent = 'Failed to load users'; emptyEl.style.display = 'block'; } showStatus('Failed to load users', 'error'); } } function displayUsers(users) { const container = document.querySelector('.users-list'); if (!container) return; // Find or create the users table container, preserving the header let usersTableContainer = container.querySelector('.users-table-container'); if (!usersTableContainer) { // If container doesn't exist, create it after the header const header = container.querySelector('.users-list-header'); usersTableContainer = document.createElement('div'); usersTableContainer.className = 'users-table-container'; if (header && header.nextSibling) { container.insertBefore(usersTableContainer, header.nextSibling); } else if (header) { container.appendChild(usersTableContainer); } else { container.appendChild(usersTableContainer); } } if (!users || users.length === 0) { usersTableContainer.innerHTML = '

No users found.

'; return; } const tableHtml = `
${users.map(user => { const createdDate = user.created_at || user['Created At'] || user.createdAt; const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; const isAdmin = user.admin || user.Admin || false; const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user'); const userId = user.Id || user.id || user.ID; // Handle expiration info let expirationInfo = ''; if (user.ExpiresAt) { const expirationDate = new Date(user.ExpiresAt); const now = new Date(); const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24)); if (daysUntilExpiration < 0) { expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago`; } else if (daysUntilExpiration <= 3) { expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}`; } else { expirationInfo = `Expires: ${expirationDate.toLocaleDateString()}`; } } return ` `; }).join('')}
Email Name Role Created Actions
${escapeHtml(user.email || user.Email || 'N/A')} ${escapeHtml(user.name || user.Name || 'N/A')} ${userType.charAt(0).toUpperCase() + userType.slice(1)} ${expirationInfo} ${formattedDate}
`; usersTableContainer.innerHTML = tableHtml; setupUserActionListeners(); } function setupUserActionListeners() { const container = document.querySelector('.users-list'); if (!container) return; // Remove existing event listeners by cloning the container const newContainer = container.cloneNode(true); container.parentNode.replaceChild(newContainer, container); // Get the updated reference const updatedContainer = document.querySelector('.users-list'); updatedContainer.addEventListener('click', function(e) { if (e.target.classList.contains('delete-user-btn')) { const userId = e.target.getAttribute('data-user-id'); const userEmail = e.target.getAttribute('data-user-email'); console.log('Delete button clicked for user:', userId); deleteUser(userId, userEmail); } else if (e.target.classList.contains('send-login-btn')) { const userId = e.target.getAttribute('data-user-id'); const userEmail = e.target.getAttribute('data-user-email'); console.log('Send login details button clicked for user:', userId); sendLoginDetailsToUser(userId, userEmail); } else if (e.target.id === 'email-all-users-btn') { console.log('Email All Users button clicked'); showEmailUsersModal(); } }); } async function deleteUser(userId, userEmail) { if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) { return; } try { const response = await fetch(`/api/users/${userId}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { showStatus(`User "${userEmail}" deleted successfully`, 'success'); loadUsers(); // Reload the users list } else { throw new Error(data.error || 'Failed to delete user'); } } catch (error) { console.error('Error deleting user:', error); showStatus(`Failed to delete user: ${error.message}`, 'error'); } } async function sendLoginDetailsToUser(userId, userEmail) { if (!confirm(`Send login details to "${userEmail}"?`)) { return; } try { const response = await fetch(`/api/users/${userId}/send-login-details`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showStatus(`Login details sent to "${userEmail}" successfully`, 'success'); } else { throw new Error(data.error || 'Failed to send login details'); } } catch (error) { console.error('Error sending login details:', error); showStatus(`Failed to send login details: ${error.message}`, 'error'); } } async function createUser(e) { e.preventDefault(); const email = document.getElementById('user-email').value.trim(); const password = document.getElementById('user-password').value; const name = document.getElementById('user-name').value.trim(); const userType = document.getElementById('user-type').value; const expireDays = userType === 'temp' ? parseInt(document.getElementById('user-expire-days').value) : null; const admin = document.getElementById('user-is-admin').checked; if (!email || !password) { showStatus('Email and password are required', 'error'); return; } if (password.length < 6) { showStatus('Password must be at least 6 characters long', 'error'); return; } if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) { showStatus('Expiration days must be between 1 and 365 for temporary users', 'error'); return; } try { const userData = { email, password, name: name || '', isAdmin: userType === 'admin' || admin, userType, expireDays }; const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await response.json(); if (data.success) { showStatus('User created successfully', 'success'); clearUserForm(); loadUsers(); // Reload the users list } else { throw new Error(data.error || 'Failed to create user'); } } catch (error) { console.error('Error creating user:', error); showStatus(`Failed to create user: ${error.message}`, 'error'); } } function clearUserForm() { const form = document.getElementById('create-user-form'); if (form) { form.reset(); // Reset user type to default const userTypeSelect = document.getElementById('user-type'); if (userTypeSelect) { userTypeSelect.value = 'user'; } // Hide expiration group const expirationGroup = document.getElementById('expiration-group'); if (expirationGroup) { expirationGroup.style.display = 'none'; } // Re-enable admin checkbox const isAdminCheckbox = document.getElementById('user-is-admin'); if (isAdminCheckbox) { isAdminCheckbox.disabled = false; } showStatus('User form cleared', 'info'); } } // Email All Users Functions let allUsersData = []; async function showEmailUsersModal() { // Load current users data try { const response = await fetch('/api/users'); const data = await response.json(); if (data.success && data.users) { allUsersData = data.users; // Update recipients count const recipientsCount = document.getElementById('recipients-count'); if (recipientsCount) { recipientsCount.textContent = `${allUsersData.length}`; } } } catch (error) { console.error('Error loading users for email:', error); showStatus('Failed to load user data', 'error'); return; } // Show modal const modal = document.getElementById('email-users-modal'); if (modal) { modal.style.display = 'flex'; // Clear previous content document.getElementById('email-subject').value = ''; document.getElementById('email-content').innerHTML = ''; document.getElementById('show-preview').checked = false; document.getElementById('email-preview').style.display = 'none'; } } function closeEmailUsersModal() { const modal = document.getElementById('email-users-modal'); if (modal) { modal.style.display = 'none'; } } function setupRichTextEditor() { const toolbar = document.querySelector('.rich-text-toolbar'); const editor = document.getElementById('email-content'); if (!toolbar || !editor) return; // Handle toolbar button clicks toolbar.addEventListener('click', (e) => { if (e.target.classList.contains('toolbar-btn')) { e.preventDefault(); const command = e.target.getAttribute('data-command'); if (command === 'createLink') { const url = prompt('Enter the URL:'); if (url) { document.execCommand(command, false, url); } } else { document.execCommand(command, false, null); } // Update preview if visible updateEmailPreview(); } }); // Update preview on content change editor.addEventListener('input', updateEmailPreview); // Handle preview toggle const showPreviewCheckbox = document.getElementById('show-preview'); if (showPreviewCheckbox) { showPreviewCheckbox.addEventListener('change', togglePreview); } // Update preview when subject changes const subjectInput = document.getElementById('email-subject'); if (subjectInput) { subjectInput.addEventListener('input', updateEmailPreview); } } function togglePreview() { const preview = document.getElementById('email-preview'); const checkbox = document.getElementById('show-preview'); if (preview && checkbox) { if (checkbox.checked) { preview.style.display = 'block'; updateEmailPreview(); } else { preview.style.display = 'none'; } } } function updateEmailPreview() { const previewSubject = document.getElementById('preview-subject'); const previewBody = document.getElementById('preview-body'); const subjectInput = document.getElementById('email-subject'); const contentEditor = document.getElementById('email-content'); if (previewSubject && subjectInput) { previewSubject.textContent = subjectInput.value || 'Your subject will appear here'; } if (previewBody && contentEditor) { const content = contentEditor.innerHTML || 'Your message will appear here'; previewBody.innerHTML = content; } } async function sendEmailToAllUsers(e) { e.preventDefault(); const subject = document.getElementById('email-subject').value.trim(); const content = document.getElementById('email-content').innerHTML.trim(); if (!subject) { showStatus('Please enter an email subject', 'error'); return; } if (!content || content === '
' || content === '') { showStatus('Please enter email content', 'error'); return; } if (allUsersData.length === 0) { showStatus('No users found to email', 'error'); return; } const confirmMessage = `Send this email to all ${allUsersData.length} users?`; if (!confirm(confirmMessage)) { return; } // Initialize progress tracking initializeEmailProgress(allUsersData.length); try { const response = await fetch('/api/users/email-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subject: subject, content: content }) }); const data = await response.json(); if (data.success) { // Display detailed results updateEmailProgress(data.results); showStatus(data.message, 'success'); console.log('Email results:', data.results); } else { showEmailError(data.error || 'Failed to send emails'); if (data.details) { console.error('Failed email details:', data.details); } } } catch (error) { console.error('Error sending emails to all users:', error); showEmailError('Failed to send emails - Network error'); } } // Initialize email progress display function initializeEmailProgress(totalCount) { const progressContainer = document.getElementById('email-progress-container'); const statusList = document.getElementById('email-status-list'); const pendingCountEl = document.getElementById('pending-count'); const successCountEl = document.getElementById('success-count'); const errorCountEl = document.getElementById('error-count'); const progressBar = document.getElementById('email-progress-bar'); const progressText = document.getElementById('progress-text'); const closeBtn = document.getElementById('close-progress-btn'); // Show progress container progressContainer.classList.add('show'); // Reset counters pendingCountEl.textContent = totalCount; successCountEl.textContent = '0'; errorCountEl.textContent = '0'; // Reset progress bar progressBar.style.width = '0%'; progressBar.classList.remove('complete', 'error'); progressText.textContent = '0%'; // Clear status list statusList.innerHTML = ''; // Hide close button initially closeBtn.style.display = 'none'; // Add status items for each user allUsersData.forEach(user => { const statusItem = document.createElement('div'); statusItem.className = 'email-status-item'; statusItem.innerHTML = `
${user.Name || user.Email}
Sending...
`; statusList.appendChild(statusItem); }); } // Update progress with results function updateEmailProgress(results) { const statusList = document.getElementById('email-status-list'); const pendingCountEl = document.getElementById('pending-count'); const successCountEl = document.getElementById('success-count'); const errorCountEl = document.getElementById('error-count'); const progressBar = document.getElementById('email-progress-bar'); const progressText = document.getElementById('progress-text'); const closeBtn = document.getElementById('close-progress-btn'); const successful = results.successful || []; const failed = results.failed || []; const total = results.total || (successful.length + failed.length); // Update counters successCountEl.textContent = successful.length; errorCountEl.textContent = failed.length; pendingCountEl.textContent = '0'; // Update progress bar const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); progressBar.style.width = percentage + '%'; progressText.textContent = percentage + '%'; if (failed.length > 0) { progressBar.classList.add('error'); } else { progressBar.classList.add('complete'); } // Update individual status items const statusItems = statusList.children; // Update successful emails successful.forEach(result => { const statusItem = Array.from(statusItems).find(item => item.querySelector('.email-status-recipient').textContent.includes(result.email) || item.querySelector('.email-status-recipient').textContent.includes(result.name) ); if (statusItem) { statusItem.querySelector('.email-status-result').innerHTML = ` ✓ Sent `; } }); // Update failed emails failed.forEach(result => { const statusItem = Array.from(statusItems).find(item => item.querySelector('.email-status-recipient').textContent.includes(result.email) || item.querySelector('.email-status-recipient').textContent.includes(result.name) ); if (statusItem) { statusItem.querySelector('.email-status-result').innerHTML = ` ✗ Failed `; } }); // Show close button closeBtn.style.display = 'block'; closeBtn.onclick = () => { document.getElementById('email-progress-container').classList.remove('show'); closeEmailUsersModal(); }; } // Show email error function showEmailError(message) { const progressContainer = document.getElementById('email-progress-container'); const progressBar = document.getElementById('email-progress-bar'); const progressText = document.getElementById('progress-text'); const closeBtn = document.getElementById('close-progress-btn'); // Show progress container if not visible progressContainer.classList.add('show'); // Update progress bar to show error progressBar.style.width = '100%'; progressBar.classList.add('error'); progressText.textContent = 'Error'; // Show close button closeBtn.style.display = 'block'; closeBtn.onclick = () => { progressContainer.classList.remove('show'); }; showStatus(message, 'error'); } // Initialize NocoDB links in admin panel async function initializeNocodbLinks() { console.log('Starting NocoDB links initialization...'); try { // Since we're in the admin panel, the user is already verified as admin // by the requireAdmin middleware. Let's get the URLs from the server directly. console.log('Fetching NocoDB URLs for admin panel...'); const configResponse = await fetch('/api/admin/nocodb-urls'); if (!configResponse.ok) { throw new Error(`NocoDB URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); } const config = await configResponse.json(); console.log('NocoDB URLs received:', config); if (config.success && config.nocodbUrls) { console.log('Setting up NocoDB links with URLs:', config.nocodbUrls); // Set up admin dashboard NocoDB links setAdminNocodbLink('admin-nocodb-view-link', config.nocodbUrls.viewUrl); setAdminNocodbLink('admin-nocodb-login-link', config.nocodbUrls.loginSheet); setAdminNocodbLink('admin-nocodb-settings-link', config.nocodbUrls.settingsSheet); setAdminNocodbLink('admin-nocodb-shifts-link', config.nocodbUrls.shiftsSheet); setAdminNocodbLink('admin-nocodb-signups-link', config.nocodbUrls.shiftSignupsSheet); console.log('NocoDB links initialized in admin panel'); } else { console.warn('No NocoDB URLs found in admin config response'); // Hide the NocoDB section if no URLs are available const nocodbSection = document.getElementById('nocodb-links'); const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); if (nocodbSection) { nocodbSection.style.display = 'none'; console.log('Hidden NocoDB section'); } if (nocodbNav) { nocodbNav.style.display = 'none'; console.log('Hidden NocoDB nav link'); } } } catch (error) { console.error('Error initializing NocoDB links in admin panel:', error); // Hide the NocoDB section on error const nocodbSection = document.getElementById('nocodb-links'); const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); if (nocodbSection) { nocodbSection.style.display = 'none'; console.log('Hidden NocoDB section due to error'); } if (nocodbNav) { nocodbNav.style.display = 'none'; console.log('Hidden NocoDB nav link due to error'); } } } // Helper function to set admin NocoDB link href function setAdminNocodbLink(elementId, url) { console.log(`Setting up NocoDB link: ${elementId} = ${url}`); const element = document.getElementById(elementId); if (element && url) { element.href = url; element.style.display = 'inline-flex'; // Remove any disabled state element.classList.remove('btn-disabled'); element.removeAttribute('disabled'); console.log(`✓ Successfully set up ${elementId}`); } else if (element) { element.style.display = 'none'; // Add disabled state if no URL element.classList.add('btn-disabled'); element.setAttribute('disabled', 'disabled'); element.href = '#'; console.log(`⚠ Disabled ${elementId} - no URL provided`); } else { console.error(`✗ Element not found: ${elementId}`); } } // Initialize Listmonk links in admin panel async function initializeListmonkLinks() { console.log('Starting Listmonk links initialization...'); try { // Since we're in the admin panel, the user is already verified as admin // by the requireAdmin middleware. Let's get the URLs from the server directly. console.log('Fetching Listmonk URLs for admin panel...'); const configResponse = await fetch('/api/admin/listmonk-urls'); if (!configResponse.ok) { throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); } const config = await configResponse.json(); console.log('Listmonk URLs received:', config); if (config.success && config.listmonkUrls) { console.log('Setting up Listmonk links with URLs:', config.listmonkUrls); // Set up admin dashboard Listmonk links setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl); setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl); setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl); setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl); setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl); console.log('Listmonk links initialized in admin panel'); } else { console.warn('No Listmonk URLs found in admin config response'); // Hide the Listmonk section if no URLs are available const listmonkSection = document.getElementById('listmonk-links'); const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); if (listmonkSection) { listmonkSection.style.display = 'none'; console.log('Hidden Listmonk section'); } if (listmonkNav) { listmonkNav.style.display = 'none'; console.log('Hidden Listmonk nav link'); } } } catch (error) { console.error('Error initializing Listmonk links in admin panel:', error); // Hide the Listmonk section on error const listmonkSection = document.getElementById('listmonk-links'); const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); if (listmonkSection) { listmonkSection.style.display = 'none'; console.log('Hidden Listmonk section due to error'); } if (listmonkNav) { listmonkNav.style.display = 'none'; console.log('Hidden Listmonk nav link due to error'); } } } // Helper function to set admin Listmonk link href function setAdminListmonkLink(elementId, url) { console.log(`Setting up Listmonk link: ${elementId} = ${url}`); const element = document.getElementById(elementId); if (element && url) { element.href = url; element.style.display = 'inline-flex'; // Remove any disabled state element.classList.remove('btn-disabled'); element.removeAttribute('disabled'); console.log(`✓ Successfully set up ${elementId}`); } else if (element) { element.style.display = 'none'; // Add disabled state if no URL element.classList.add('btn-disabled'); element.setAttribute('disabled', 'disabled'); element.href = '#'; console.log(`⚠ Disabled ${elementId} - no URL provided`); } else { console.error(`✗ Element not found: ${elementId}`); } } // Shift User Management Functions let currentShiftData = null; let allUsers = []; // Load all users for the dropdown async function loadAllUsers() { try { const response = await fetch('/api/users'); const data = await response.json(); if (data.success) { allUsers = data.users; populateUserSelect(); } else { console.error('Failed to load users:', data.error); } } catch (error) { console.error('Error loading users:', error); } } // Populate user select dropdown function populateUserSelect() { const select = document.getElementById('user-select'); if (!select) return; // Clear existing options except the first one select.innerHTML = ''; allUsers.forEach(user => { const option = document.createElement('option'); option.value = user.email || user.Email; option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`; select.appendChild(option); }); } // Show the shift user management modal async function showShiftUserModal(shiftId, shiftData) { currentShiftData = { ...shiftData, ID: shiftId }; // Update modal title and info document.getElementById('modal-shift-title').textContent = shiftData.Title; const shiftDate = createLocalDate(shiftData.Date); document.getElementById('modal-shift-details').textContent = `${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`; // Load users if not already loaded if (allUsers.length === 0) { await loadAllUsers(); } // Display current volunteers displayCurrentVolunteers(shiftData.signups || []); // Show modal document.getElementById('shift-user-modal').style.display = 'flex'; } // Display current volunteers in the modal function displayCurrentVolunteers(volunteers) { const container = document.getElementById('current-volunteers-list'); if (!volunteers || volunteers.length === 0) { container.innerHTML = '
No volunteers signed up yet.
'; return; } container.innerHTML = volunteers.map(volunteer => `
${escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}
${escapeHtml(volunteer['User Email'])}
`).join(''); // Add event listeners for remove buttons setupVolunteerActionListeners(); } // Setup event listeners for volunteer actions function setupVolunteerActionListeners() { const container = document.getElementById('current-volunteers-list'); container.addEventListener('click', function(e) { if (e.target.classList.contains('remove-volunteer-btn')) { const volunteerId = e.target.getAttribute('data-volunteer-id'); const volunteerEmail = e.target.getAttribute('data-volunteer-email'); removeVolunteerFromShift(volunteerId, volunteerEmail); } }); } // Add user to shift async function addUserToShift() { const userSelect = document.getElementById('user-select'); const userEmail = userSelect.value; if (!userEmail) { showStatus('Please select a user to add', 'error'); return; } if (!currentShiftData || !currentShiftData.ID) { showStatus('No shift selected or invalid shift data', 'error'); console.error('Invalid currentShiftData:', currentShiftData); return; } try { const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userEmail }) }); const data = await response.json(); if (data.success) { showStatus('User successfully added to shift', 'success'); userSelect.value = ''; // Clear selection // Refresh the shift data and reload volunteers with better error handling try { await refreshCurrentShiftData(); console.log('Refreshed shift data after adding user'); } catch (refreshError) { console.error('Error during refresh after adding user:', refreshError); // Still show success since the add operation worked } } else { showStatus(data.error || 'Failed to add user to shift', 'error'); } } catch (error) { console.error('Error adding user to shift:', error); showStatus('Failed to add user to shift', 'error'); } } // Remove volunteer from shift async function removeVolunteerFromShift(volunteerId, volunteerEmail) { if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) { return; } if (!currentShiftData || !currentShiftData.ID) { showStatus('No shift selected or invalid shift data', 'error'); console.error('Invalid currentShiftData:', currentShiftData); return; } try { const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { showStatus('Volunteer successfully removed from shift', 'success'); // Refresh the shift data and reload volunteers with better error handling try { await refreshCurrentShiftData(); console.log('Refreshed shift data after removing volunteer'); } catch (refreshError) { console.error('Error during refresh after removing volunteer:', refreshError); // Still show success since the remove operation worked } } else { showStatus(data.error || 'Failed to remove volunteer from shift', 'error'); } } catch (error) { console.error('Error removing volunteer from shift:', error); showStatus('Failed to remove volunteer from shift', 'error'); } } // Refresh current shift data async function refreshCurrentShiftData() { if (!currentShiftData || !currentShiftData.ID) { console.warn('No current shift data or missing ID, skipping refresh'); return; } try { console.log('Refreshing shift data for shift ID:', currentShiftData.ID); // Instead of reloading ALL admin shifts, just get this specific shift's signups // This prevents the expensive backend call and reduces the refresh cascade const response = await fetch(`/api/shifts/admin`); const data = await response.json(); if (data.success && data.shifts && Array.isArray(data.shifts)) { const updatedShift = data.shifts.find(s => s && s.ID === currentShiftData.ID); if (updatedShift) { console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers'); currentShiftData = updatedShift; displayCurrentVolunteers(updatedShift.signups || []); // Only update the specific shift in the main list, don't refresh everything updateShiftInList(updatedShift); } else { console.warn('Could not find updated shift with ID:', currentShiftData.ID); } } else { console.error('Failed to refresh shift data:', data.error || 'Invalid response format'); } } catch (error) { console.error('Error refreshing shift data:', error); } } // New function to update a single shift in the list without full refresh function updateShiftInList(updatedShift) { const shiftElement = document.querySelector(`[data-shift-id="${updatedShift.ID}"]`); if (shiftElement) { const shiftItem = shiftElement.closest('.shift-admin-item'); if (shiftItem) { const signupCount = updatedShift.signups ? updatedShift.signups.length : 0; // Find the volunteer count paragraph (contains 👥) const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => p.textContent.includes('👥') ); if (volunteerCountElement) { volunteerCountElement.textContent = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers`; } // Update the data attribute with new shift data const manageBtn = shiftItem.querySelector('.manage-volunteers-btn'); if (manageBtn) { manageBtn.setAttribute('data-shift', JSON.stringify(updatedShift).replace(/'/g, "'")); } } } } // Close modal function closeShiftUserModal() { document.getElementById('shift-user-modal').style.display = 'none'; currentShiftData = null; // Don't refresh the entire shifts list when closing modal // The shifts list should already be up to date from the individual updates console.log('Modal closed - shifts list should already be current'); } // Email shift details to all volunteers async function emailShiftDetails() { if (!currentShiftData) { showStatus('No shift selected', 'error'); return; } // Check if there are volunteers to email const volunteers = currentShiftData.signups || []; if (volunteers.length === 0) { showStatus('No volunteers signed up for this shift', 'error'); return; } // Confirm action const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`; if (!confirm(confirmMessage)) { return; } // Initialize progress tracking for shift emails initializeShiftEmailProgress(volunteers.length); try { const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { // Display detailed results updateShiftEmailProgress(data.results); showStatus(data.message, 'success'); console.log('Email results:', data.results); } else { showShiftEmailError(data.error || 'Failed to send emails'); if (data.details) { console.error('Failed email details:', data.details); } } } catch (error) { console.error('Error sending shift details emails:', error); showShiftEmailError('Failed to send emails - Network error'); } } // Initialize shift email progress display function initializeShiftEmailProgress(totalCount) { const progressContainer = document.getElementById('shift-email-progress-container'); const statusList = document.getElementById('shift-email-status-list'); const pendingCountEl = document.getElementById('shift-pending-count'); const successCountEl = document.getElementById('shift-success-count'); const errorCountEl = document.getElementById('shift-error-count'); const progressBar = document.getElementById('shift-email-progress-bar'); const progressText = document.getElementById('shift-progress-text'); const closeBtn = document.getElementById('close-shift-progress-btn'); // Show progress container progressContainer.classList.add('show'); // Reset counters pendingCountEl.textContent = totalCount; successCountEl.textContent = '0'; errorCountEl.textContent = '0'; // Reset progress bar progressBar.style.width = '0%'; progressBar.classList.remove('complete', 'error'); progressText.textContent = '0%'; // Clear status list statusList.innerHTML = ''; // Hide close button initially closeBtn.style.display = 'none'; // Add status items for each volunteer const volunteers = currentShiftData.signups || []; volunteers.forEach(volunteer => { const statusItem = document.createElement('div'); statusItem.className = 'email-status-item'; statusItem.innerHTML = `
${volunteer['User Name'] || volunteer['User Email']}
Sending...
`; statusList.appendChild(statusItem); }); } // Update shift email progress with results function updateShiftEmailProgress(results) { const statusList = document.getElementById('shift-email-status-list'); const pendingCountEl = document.getElementById('shift-pending-count'); const successCountEl = document.getElementById('shift-success-count'); const errorCountEl = document.getElementById('shift-error-count'); const progressBar = document.getElementById('shift-email-progress-bar'); const progressText = document.getElementById('shift-progress-text'); const closeBtn = document.getElementById('close-shift-progress-btn'); const successful = results.successful || []; const failed = results.failed || []; const total = results.total || (successful.length + failed.length); // Update counters successCountEl.textContent = successful.length; errorCountEl.textContent = failed.length; pendingCountEl.textContent = '0'; // Update progress bar const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); progressBar.style.width = percentage + '%'; progressText.textContent = percentage + '%'; if (failed.length > 0) { progressBar.classList.add('error'); } else { progressBar.classList.add('complete'); } // Update individual status items const statusItems = statusList.children; // Update successful emails successful.forEach(result => { const statusItem = Array.from(statusItems).find(item => item.querySelector('.email-status-recipient').textContent.includes(result.email) || item.querySelector('.email-status-recipient').textContent.includes(result.name) ); if (statusItem) { statusItem.querySelector('.email-status-result').innerHTML = ` ✓ Sent `; } }); // Update failed emails failed.forEach(result => { const statusItem = Array.from(statusItems).find(item => item.querySelector('.email-status-recipient').textContent.includes(result.email) || item.querySelector('.email-status-recipient').textContent.includes(result.name) ); if (statusItem) { statusItem.querySelector('.email-status-result').innerHTML = ` ✗ Failed `; } }); // Show close button closeBtn.style.display = 'block'; closeBtn.onclick = () => { document.getElementById('shift-email-progress-container').classList.remove('show'); }; } // Show shift email error function showShiftEmailError(message) { const progressContainer = document.getElementById('shift-email-progress-container'); const progressBar = document.getElementById('shift-email-progress-bar'); const progressText = document.getElementById('shift-progress-text'); const closeBtn = document.getElementById('close-shift-progress-btn'); // Show progress container if not visible progressContainer.classList.add('show'); // Update progress bar to show error progressBar.style.width = '100%'; progressBar.classList.add('error'); progressText.textContent = 'Error'; // Show close button closeBtn.style.display = 'block'; closeBtn.onclick = () => { progressContainer.classList.remove('show'); }; showStatus(message, 'error'); } // Setup modal event listeners when DOM is loaded document.addEventListener('DOMContentLoaded', function() { const closeModalBtn = document.getElementById('close-user-modal'); const addUserBtn = document.getElementById('add-user-btn'); const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn'); const modal = document.getElementById('shift-user-modal'); if (closeModalBtn) { closeModalBtn.addEventListener('click', closeShiftUserModal); } if (addUserBtn) { addUserBtn.addEventListener('click', addUserToShift); } if (emailShiftDetailsBtn) { emailShiftDetailsBtn.addEventListener('click', emailShiftDetails); } // Close modal when clicking outside if (modal) { modal.addEventListener('click', function(e) { if (e.target === modal) { closeShiftUserModal(); } }); } }); // Setup email users modal event listeners when DOM is loaded document.addEventListener('DOMContentLoaded', function() { // Email all users functionality const closeEmailModalBtn = document.getElementById('close-email-modal'); const cancelEmailBtn = document.getElementById('cancel-email-btn'); const emailUsersForm = document.getElementById('email-users-form'); const emailModal = document.getElementById('email-users-modal'); if (closeEmailModalBtn) { closeEmailModalBtn.addEventListener('click', closeEmailUsersModal); } if (cancelEmailBtn) { cancelEmailBtn.addEventListener('click', closeEmailUsersModal); } if (emailUsersForm) { emailUsersForm.addEventListener('submit', sendEmailToAllUsers); } // Close modal when clicking outside if (emailModal) { emailModal.addEventListener('click', function(e) { if (e.target === emailModal) { closeEmailUsersModal(); } }); } // Setup rich text editor functionality setupRichTextEditor(); }); // Public Shifts Functions function generateShiftPublicLink(shiftId) { const baseUrl = window.location.origin; return `${baseUrl}/public-shifts.html#shift-${shiftId}`; } function copyShiftLink(shiftId) { const link = generateShiftPublicLink(shiftId); navigator.clipboard.writeText(link).then(() => { showStatus('Public shift link copied to clipboard!', 'success'); }).catch(() => { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = link; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showStatus('Public shift link copied to clipboard!', 'success'); }); } function openShiftLink(shiftId) { const link = generateShiftPublicLink(shiftId); window.open(link, '_blank'); } // Update the shift form to include Is Public checkbox function updateShiftFormWithPublicOption() { const form = document.getElementById('shift-form'); if (!form) return; // Check if public checkbox already exists if (document.getElementById('shift-is-public')) return; const maxVolunteersGroup = form.querySelector('.form-group:has(#shift-max-volunteers)'); if (maxVolunteersGroup) { const publicGroup = document.createElement('div'); publicGroup.className = 'form-group'; publicGroup.innerHTML = ` `; maxVolunteersGroup.insertAdjacentElement('afterend', publicGroup); } } // Call this when the shifts section is shown function enhanceShiftsSection() { updateShiftFormWithPublicOption(); } // Update the showSection function to call enhanceShiftsSection when shifts section is shown const originalShowSection = window.showSection || showSection; window.showSection = function(sectionId) { if (originalShowSection) { originalShowSection(sectionId); } if (sectionId === 'shifts') { enhanceShiftsSection(); } };