/** * Admin Shift Volunteers Module * Handles volunteer management modals and shift email functionality */ // Prevent double loading if (window.adminShiftVolunteersLoading) { console.warn('admin-shift-volunteers.js already loading, skipping...'); } else { window.adminShiftVolunteersLoading = true; // Check if adminCore module is available console.log('🔄 Loading admin-shift-volunteers.js...'); console.log('📦 adminCore available:', !!window.adminCore); if (!window.adminCore) { console.error('adminCore module not found - admin-shift-volunteers.js depends on admin-core.js'); // Don't stop loading, just note the dependency issue } // Volunteer management state let currentShiftData = null; let allUsers = []; // Safe wrapper for adminCore functions function safeAdminCore(funcName, ...args) { if (window.adminCore && typeof window.adminCore[funcName] === 'function') { return window.adminCore[funcName](...args); } else { console.error(`adminCore.${funcName} not available`); return null; } } // 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 using the new function updateModalTitle(); // Load users if not already loaded if (allUsers.length === 0) { await loadAllUsers(); populateUserSelect(); } // Display current volunteers displayCurrentVolunteers(shiftData.signups || []); // Show modal const modal = document.getElementById('shift-user-modal'); if (modal) { modal.style.display = 'flex'; } } // Display current volunteers in the modal function displayCurrentVolunteers(volunteers) { const container = document.getElementById('current-volunteers-list'); if (!container) return; if (!volunteers || volunteers.length === 0) { container.innerHTML = '
No volunteers signed up yet.
'; return; } container.innerHTML = volunteers.map(volunteer => `
${safeAdminCore('escapeHtml', volunteer['User Name'] || volunteer['User Email'] || 'Unknown') || 'Unknown'}
${safeAdminCore('escapeHtml', volunteer['User Email']) || 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'); if (container) { 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); } else if (e.target.classList.contains('sms-volunteer-btn')) { const volunteerEmail = e.target.getAttribute('data-volunteer-email'); const volunteerName = e.target.getAttribute('data-volunteer-name'); openVolunteerSMS(volunteerEmail, volunteerName); } else if (e.target.classList.contains('call-volunteer-btn')) { const volunteerEmail = e.target.getAttribute('data-volunteer-email'); const volunteerName = e.target.getAttribute('data-volunteer-name'); callVolunteer(volunteerEmail, volunteerName); } }); } } // Add user to shift async function addUserToShift() { const userSelect = document.getElementById('user-select'); const userEmail = userSelect?.value; if (!userEmail) { safeAdminCore('showStatus', 'Please select a user to add', 'error'); return; } if (!currentShiftData || !currentShiftData.ID) { safeAdminCore('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) { window.adminCore.showStatus('User successfully added to shift', 'success'); if (userSelect) 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'); // Also update the modal title to reflect new volunteer count updateModalTitle(); } catch (refreshError) { console.error('Error during refresh after adding user:', refreshError); // Still show success since the add operation worked } } else { window.adminCore.showStatus(data.error || 'Failed to add user to shift', 'error'); } } catch (error) { console.error('Error adding user to shift:', error); window.adminCore.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) { window.adminCore.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) { window.adminCore.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'); // Also update the modal title to reflect new volunteer count updateModalTitle(); } catch (refreshError) { console.error('Error during refresh after removing volunteer:', refreshError); // Still show success since the remove operation worked } } else { window.adminCore.showStatus(data.error || 'Failed to remove volunteer from shift', 'error'); } } catch (error) { console.error('Error removing volunteer from shift:', error); window.adminCore.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); } } // 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; // Generate list of first names for volunteers (same logic as displayAdminShifts) const firstNames = updatedShift.signups ? updatedShift.signups.map(volunteer => { const fullName = volunteer['User Name'] || volunteer['User Email'] || 'Unknown'; // Extract first name (everything before first space, or email username if no space) const firstName = fullName.includes(' ') ? fullName.split(' ')[0] : fullName.includes('@') ? fullName.split('@')[0] : fullName; return safeAdminCore('escapeHtml', firstName) || firstName; }).slice(0, 8) : []; // Limit to first 8 names to avoid overflow const namesDisplay = firstNames.length > 0 ? `(${firstNames.join(', ')}${firstNames.length === 8 && signupCount > 8 ? '...' : ''})` : ''; // Find the volunteer count paragraph (contains 👥) const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => p.textContent.includes('👥') || p.classList.contains('volunteer-count') ); if (volunteerCountElement) { volunteerCountElement.innerHTML = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers ${namesDisplay}`; volunteerCountElement.className = 'volunteer-count'; // Ensure class is set } // 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, "'")); } } } } // Update modal title with current volunteer count function updateModalTitle() { if (!currentShiftData) return; const modalTitle = document.getElementById('modal-shift-title'); const modalDetails = document.getElementById('modal-shift-details'); if (modalTitle) { const signupCount = currentShiftData.signups ? currentShiftData.signups.length : 0; modalTitle.textContent = `Manage Volunteers - ${currentShiftData.Title} (${signupCount}/${currentShiftData['Max Volunteers']})`; } if (modalDetails) { const shiftDate = safeAdminCore('createLocalDate', currentShiftData.Date); const dateStr = shiftDate ? shiftDate.toLocaleDateString() : currentShiftData.Date; const signupCount = currentShiftData.signups ? currentShiftData.signups.length : 0; modalDetails.innerHTML = `

Date: ${dateStr}

Time: ${currentShiftData['Start Time']} - ${currentShiftData['End Time']}

Location: ${safeAdminCore('escapeHtml', currentShiftData.Location || 'TBD') || currentShiftData.Location || 'TBD'}

Current Signups: ${signupCount} / ${currentShiftData['Max Volunteers']}

`; } } // Close modal function closeShiftUserModal() { const modal = document.getElementById('shift-user-modal'); if (modal) { 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'); } // Communication functions for individual volunteers async function openVolunteerSMS(volunteerEmail, volunteerName) { try { // Look up the volunteer's phone number from the users database const user = await getUserByEmail(volunteerEmail); if (user && (user.phone || user.Phone)) { const phoneNumber = user.phone || user.Phone; const smsUrl = `sms:${phoneNumber}`; window.open(smsUrl, '_self'); } else { safeAdminCore('showStatus', `No phone number found for ${volunteerName}`, 'warning'); } } catch (error) { console.error('Error looking up volunteer phone number:', error); safeAdminCore('showStatus', 'Failed to lookup volunteer phone number', 'error'); } } async function callVolunteer(volunteerEmail, volunteerName) { try { // Look up the volunteer's phone number from the users database const user = await getUserByEmail(volunteerEmail); if (user && (user.phone || user.Phone)) { const phoneNumber = user.phone || user.Phone; const telUrl = `tel:${phoneNumber}`; window.open(telUrl, '_self'); } else { safeAdminCore('showStatus', `No phone number found for ${volunteerName}`, 'warning'); } } catch (error) { console.error('Error looking up volunteer phone number:', error); safeAdminCore('showStatus', 'Failed to lookup volunteer phone number', 'error'); } } // Helper function to get user details by email async function getUserByEmail(email) { try { const response = await fetch('/api/users'); const data = await response.json(); if (data.success && data.users) { return data.users.find(user => (user.email === email || user.Email === email) ); } return null; } catch (error) { console.error('Error fetching users:', error); return null; } } // Email shift details to all volunteers async function emailShiftDetails() { if (!currentShiftData) { window.adminCore.showStatus('No shift selected', 'error'); return; } // Check if there are volunteers to email const volunteers = currentShiftData.signups || []; if (volunteers.length === 0) { window.adminCore.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); window.adminCore.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'); if (!progressContainer) return; // Show progress container progressContainer.classList.add('show'); // Reset counters if (pendingCountEl) pendingCountEl.textContent = totalCount; if (successCountEl) successCountEl.textContent = '0'; if (errorCountEl) errorCountEl.textContent = '0'; // Reset progress bar if (progressBar) { progressBar.style.width = '0%'; progressBar.classList.remove('complete', 'error'); } if (progressText) progressText.textContent = '0%'; // Clear status list if (statusList) statusList.innerHTML = ''; // Hide close button initially if (closeBtn) closeBtn.style.display = 'none'; // Add status items for each volunteer const volunteers = currentShiftData.signups || []; volunteers.forEach(volunteer => { if (statusList) { 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 if (successCountEl) successCountEl.textContent = successful.length; if (errorCountEl) errorCountEl.textContent = failed.length; if (pendingCountEl) pendingCountEl.textContent = '0'; // Update progress bar const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); if (progressBar) { progressBar.style.width = percentage + '%'; progressText.textContent = percentage + '%'; if (failed.length > 0) { progressBar.classList.add('error'); } else { progressBar.classList.add('complete'); } } // Update individual status items if (statusList) { 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 if (closeBtn) { closeBtn.style.display = 'block'; closeBtn.onclick = () => { const progressContainer = document.getElementById('shift-email-progress-container'); if (progressContainer) { progressContainer.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 if (progressContainer) { progressContainer.classList.add('show'); } // Update progress bar to show error if (progressBar) { progressBar.style.width = '100%'; progressBar.classList.add('error'); } if (progressText) progressText.textContent = 'Error'; // Show close button if (closeBtn) { closeBtn.style.display = 'block'; closeBtn.onclick = () => { if (progressContainer) { progressContainer.classList.remove('show'); } }; } window.adminCore.showStatus(message, 'error'); } // Setup volunteer modal event listeners function setupVolunteerModalEventListeners() { 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(); } }); } } // Export shift volunteer functions console.log('About to export adminShiftVolunteers module...'); console.log('Functions to export:', { showShiftUserModal: typeof showShiftUserModal, closeShiftUserModal: typeof closeShiftUserModal, addUserToShift: typeof addUserToShift, removeVolunteerFromShift: typeof removeVolunteerFromShift, emailShiftDetails: typeof emailShiftDetails, setupVolunteerModalEventListeners: typeof setupVolunteerModalEventListeners, loadAllUsers: typeof loadAllUsers }); // Ensure we have all required functions before exporting const requiredFunctions = { showShiftUserModal, closeShiftUserModal, addUserToShift, removeVolunteerFromShift, emailShiftDetails, setupVolunteerModalEventListeners, loadAllUsers }; // Validate all functions exist const missingFunctions = Object.entries(requiredFunctions) .filter(([name, func]) => typeof func !== 'function') .map(([name]) => name); if (missingFunctions.length > 0) { console.error('Missing functions in adminShiftVolunteers:', missingFunctions); } else { console.log('All required functions are available'); } try { // Clear any existing module to avoid conflicts if (window.adminShiftVolunteers) { console.log('Replacing existing adminShiftVolunteers module'); } window.adminShiftVolunteers = { showShiftUserModal, closeShiftUserModal, addUserToShift, removeVolunteerFromShift, emailShiftDetails, setupVolunteerModalEventListeners, loadAllUsers, openVolunteerSMS, callVolunteer, getUserByEmail, updateModalTitle, updateShiftInList, getCurrentShiftData: () => currentShiftData, getAllUsers: () => allUsers, // Add module info for debugging moduleVersion: '1.2', loadedAt: new Date().toISOString() }; // Verify the export worked if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.showShiftUserModal === 'function') { console.log('✅ adminShiftVolunteers module loaded successfully'); console.log('✅ Available functions:', Object.keys(window.adminShiftVolunteers)); // Dispatch a custom event to signal the module is ready setTimeout(() => { window.dispatchEvent(new CustomEvent('adminShiftVolunteersReady', { detail: { module: 'adminShiftVolunteers', functions: Object.keys(window.adminShiftVolunteers), timestamp: Date.now() } })); console.log('✅ adminShiftVolunteersReady event dispatched'); }, 50); } else { throw new Error('Module export verification failed'); } } catch (error) { console.error('❌ Error loading adminShiftVolunteers module:', error); console.error('❌ Stack trace:', error.stack); } finally { // Mark loading as complete window.adminShiftVolunteersLoading = false; } // Add a global debugging function window.debugAdminShiftVolunteers = () => { console.log('🔍 Admin Shift Volunteers Debug Info:'); console.log('Module exists:', !!window.adminShiftVolunteers); if (window.adminShiftVolunteers) { console.log('Available functions:', Object.keys(window.adminShiftVolunteers)); console.log('showShiftUserModal type:', typeof window.adminShiftVolunteers.showShiftUserModal); console.log('Module version:', window.adminShiftVolunteers.moduleVersion); console.log('Loaded at:', window.adminShiftVolunteers.loadedAt); } // Check if modal elements exist console.log('Modal element exists:', !!document.getElementById('shift-user-modal')); console.log('Modal title element exists:', !!document.getElementById('modal-shift-title')); console.log('Volunteers list element exists:', !!document.getElementById('current-volunteers-list')); // Check dependencies console.log('adminCore exists:', !!window.adminCore); console.log('Loading state:', window.adminShiftVolunteersLoading); return { moduleExists: !!window.adminShiftVolunteers, functions: window.adminShiftVolunteers ? Object.keys(window.adminShiftVolunteers) : [], modalExists: !!document.getElementById('shift-user-modal'), dependencies: { adminCore: !!window.adminCore } }; }; } // Close the initial if statement