// Admin panel JavaScript let adminMap = null; let startMarker = null; let storedQRCodes = {}; // Utility function to create a local date from YYYY-MM-DD string // This prevents timezone issues when displaying dates function createLocalDate(dateString) { if (!dateString) return null; const parts = dateString.split('-'); if (parts.length !== 3) return new Date(dateString); // fallback to original behavior // Create date using local timezone (year, month-1, day) return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); } // A function to set viewport dimensions for admin page function setAdminViewportDimensions() { const doc = document.documentElement; // Set height and width doc.style.setProperty('--app-height', `${window.innerHeight}px`); doc.style.setProperty('--app-width', `${window.innerWidth}px`); // Handle safe area insets for devices with notches or home indicators if (CSS.supports('padding: env(safe-area-inset-top)')) { doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)'); doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)'); doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)'); doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)'); } else { doc.style.setProperty('--safe-area-top', '0px'); doc.style.setProperty('--safe-area-bottom', '0px'); doc.style.setProperty('--safe-area-left', '0px'); doc.style.setProperty('--safe-area-right', '0px'); } } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', () => { // Set initial viewport dimensions and listen for resize events setAdminViewportDimensions(); window.addEventListener('resize', setAdminViewportDimensions); window.addEventListener('orientationchange', () => { // Add a small delay for orientation change to complete setTimeout(setAdminViewportDimensions, 100); }); checkAdminAuth(); initializeAdminMap(); loadCurrentStartLocation(); setupEventListeners(); setupNavigation(); setupMobileMenu(); // Initialize NocoDB links with a small delay to ensure DOM is ready setTimeout(() => { loadWalkSheetConfig(); initializeNocodbLinks(); initializeListmonkLinks(); }, 100); // Check if URL has a hash to show specific section const hash = window.location.hash; if (hash === '#walk-sheet') { showSection('walk-sheet'); checkAndLoadWalkSheetConfig(); } else if (hash === '#convert-data') { showSection('convert-data'); } else if (hash === '#cuts') { showSection('cuts'); } else { // Default to dashboard showSection('dashboard'); // Load dashboard data on initial page load loadDashboardData(); } }); // Add mobile menu functionality function setupMobileMenu() { const menuToggle = document.getElementById('mobile-menu-toggle'); const sidebar = document.getElementById('admin-sidebar'); const closeSidebar = document.getElementById('close-sidebar'); const adminNavLinks = document.querySelectorAll('.admin-nav a'); if (menuToggle && sidebar) { // Toggle menu menuToggle.addEventListener('click', () => { sidebar.classList.toggle('active'); menuToggle.classList.toggle('active'); document.body.classList.toggle('sidebar-open'); // This line already exists }); // Close sidebar button if (closeSidebar) { closeSidebar.addEventListener('click', () => { sidebar.classList.remove('active'); menuToggle.classList.remove('active'); document.body.classList.remove('sidebar-open'); }); } // Close sidebar when clicking outside document.addEventListener('click', (e) => { if (sidebar.classList.contains('active') && !sidebar.contains(e.target) && !menuToggle.contains(e.target)) { sidebar.classList.remove('active'); menuToggle.classList.remove('active'); document.body.classList.remove('sidebar-open'); } }); // Close sidebar when navigation link is clicked on mobile adminNavLinks.forEach(link => { link.addEventListener('click', () => { if (window.innerWidth <= 768) { sidebar.classList.remove('active'); menuToggle.classList.remove('active'); document.body.classList.remove('sidebar-open'); } }); }); } } // Check if user is authenticated as admin async function checkAdminAuth() { try { const response = await fetch('/api/auth/check'); const data = await response.json(); console.log('Admin auth check result:', data); if (!data.authenticated || !data.user?.isAdmin) { console.log('Redirecting to login - not authenticated or not admin'); window.location.href = '/login.html'; return; } console.log('User is authenticated as admin:', data.user); // Display admin info (desktop) document.getElementById('admin-info').innerHTML = ` 👤 ${escapeHtml(data.user.email)} `; // Display admin info (mobile) const mobileAdminInfo = document.getElementById('mobile-admin-info'); if (mobileAdminInfo) { mobileAdminInfo.innerHTML = `
👤 ${escapeHtml(data.user.email)}
`; // Add logout listener for mobile button document.getElementById('mobile-logout-btn')?.addEventListener('click', handleLogout); } document.getElementById('logout-btn').addEventListener('click', handleLogout); } catch (error) { console.error('Auth check failed:', error); window.location.href = '/login.html'; } } // Initialize the admin map function initializeAdminMap() { adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, minZoom: 2 }).addTo(adminMap); // Add crosshair to center of map const crosshairIcon = L.divIcon({ className: 'crosshair', iconSize: [20, 20], html: '
' }); const crosshair = L.marker(adminMap.getCenter(), { icon: crosshairIcon, interactive: false, zIndexOffset: 1000 }).addTo(adminMap); // Update crosshair position when map moves adminMap.on('move', function() { crosshair.setLatLng(adminMap.getCenter()); }); // Add click handler to set location adminMap.on('click', handleMapClick); // Update coordinates when map moves adminMap.on('moveend', updateCoordinatesFromMap); } // Load current start location async function loadCurrentStartLocation() { try { const response = await fetch('/api/admin/start-location'); const data = await response.json(); if (data.success) { const { latitude, longitude, zoom } = data.location; // Update form fields document.getElementById('start-lat').value = latitude; document.getElementById('start-lng').value = longitude; document.getElementById('start-zoom').value = zoom; // Update map adminMap.setView([latitude, longitude], zoom); updateStartMarker(latitude, longitude); // Show source info if (data.source) { const sourceText = data.source === 'database' ? 'Loaded from database' : data.source === 'environment' ? 'Using environment defaults' : 'Using system defaults'; showStatus(sourceText, 'info'); } } } catch (error) { console.error('Failed to load start location:', error); showStatus('Failed to load current start location', 'error'); } } // Handle map click function handleMapClick(e) { const { lat, lng } = e.latlng; document.getElementById('start-lat').value = lat.toFixed(6); document.getElementById('start-lng').value = lng.toFixed(6); updateStartMarker(lat, lng); } // Update marker position function updateStartMarker(lat, lng) { if (startMarker) { startMarker.setLatLng([lat, lng]); } else { startMarker = L.marker([lat, lng], { draggable: true, title: 'Start Location' }).addTo(adminMap); // Update coordinates when marker is dragged startMarker.on('dragend', (e) => { const position = e.target.getLatLng(); document.getElementById('start-lat').value = position.lat.toFixed(6); document.getElementById('start-lng').value = position.lng.toFixed(6); }); } } // Update coordinates from current map view function updateCoordinatesFromMap() { const center = adminMap.getCenter(); const zoom = adminMap.getZoom(); document.getElementById('start-zoom').value = zoom; } // Setup event listeners function setupEventListeners() { // Use current view button const useCurrentViewBtn = document.getElementById('use-current-view'); if (useCurrentViewBtn) { useCurrentViewBtn.addEventListener('click', () => { const center = adminMap.getCenter(); const zoom = adminMap.getZoom(); document.getElementById('start-lat').value = center.lat.toFixed(6); document.getElementById('start-lng').value = center.lng.toFixed(6); document.getElementById('start-zoom').value = zoom; updateStartMarker(center.lat, center.lng); showStatus('Captured current map view', 'success'); }); } // Save button const saveLocationBtn = document.getElementById('save-start-location'); if (saveLocationBtn) { saveLocationBtn.addEventListener('click', saveStartLocation); } // Coordinate input changes const startLatInput = document.getElementById('start-lat'); const startLngInput = document.getElementById('start-lng'); const startZoomInput = document.getElementById('start-zoom'); if (startLatInput) startLatInput.addEventListener('change', updateMapFromInputs); if (startLngInput) startLngInput.addEventListener('change', updateMapFromInputs); if (startZoomInput) startZoomInput.addEventListener('change', updateMapFromInputs); // Walk Sheet buttons const saveWalkSheetBtn = document.getElementById('save-walk-sheet'); const previewWalkSheetBtn = document.getElementById('preview-walk-sheet'); const printWalkSheetBtn = document.getElementById('print-walk-sheet'); const refreshPreviewBtn = document.getElementById('refresh-preview'); if (saveWalkSheetBtn) saveWalkSheetBtn.addEventListener('click', saveWalkSheetConfig); if (previewWalkSheetBtn) previewWalkSheetBtn.addEventListener('click', generateWalkSheetPreview); if (printWalkSheetBtn) printWalkSheetBtn.addEventListener('click', printWalkSheet); if (refreshPreviewBtn) refreshPreviewBtn.addEventListener('click', generateWalkSheetPreview); // Auto-update preview on input change const walkSheetInputs = document.querySelectorAll( '#walk-sheet-title, #walk-sheet-subtitle, #walk-sheet-footer, ' + '[id^="qr-code-"][id$="-url"], [id^="qr-code-"][id$="-label"]' ); walkSheetInputs.forEach(input => { if (input) { input.addEventListener('input', debounce(() => { generateWalkSheetPreview(); }, 500)); } }); // Add URL change listeners to detect when QR codes need regeneration for (let i = 1; i <= 3; i++) { const urlInput = document.getElementById(`qr-code-${i}-url`); if (urlInput) { let previousUrl = urlInput.value; urlInput.addEventListener('change', () => { const currentUrl = urlInput.value; if (currentUrl !== previousUrl) { console.log(`QR Code ${i} URL changed from "${previousUrl}" to "${currentUrl}"`); // Remove stored QR code so it gets regenerated delete storedQRCodes[currentUrl]; previousUrl = currentUrl; generateWalkSheetPreview(); } }); } } // Shift form submission const shiftForm = document.getElementById('shift-form'); if (shiftForm) { shiftForm.addEventListener('submit', createShift); } // Clear shift form button const clearShiftBtn = document.getElementById('clear-shift-form'); if (clearShiftBtn) { clearShiftBtn.addEventListener('click', clearShiftForm); } // User form submission const userForm = document.getElementById('create-user-form'); if (userForm) { userForm.addEventListener('submit', createUser); } // Clear user form button const clearUserBtn = document.getElementById('clear-user-form'); if (clearUserBtn) { clearUserBtn.addEventListener('click', clearUserForm); } // User type change listener const userTypeSelect = document.getElementById('user-type'); if (userTypeSelect) { userTypeSelect.addEventListener('change', (e) => { const expirationGroup = document.getElementById('expiration-group'); const isAdminCheckbox = document.getElementById('user-is-admin'); if (e.target.value === 'temp') { expirationGroup.style.display = 'block'; isAdminCheckbox.checked = false; isAdminCheckbox.disabled = true; } else { expirationGroup.style.display = 'none'; isAdminCheckbox.disabled = false; if (e.target.value === 'admin') { isAdminCheckbox.checked = true; } else { isAdminCheckbox.checked = false; } } }); } } // Setup navigation between admin sections function setupNavigation() { const navLinks = document.querySelectorAll('.admin-nav a'); const sections = document.querySelectorAll('.admin-section'); navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('href').substring(1); // Update active nav navLinks.forEach(l => l.classList.remove('active')); link.classList.add('active'); // Show target section sections.forEach(section => { section.style.display = section.id === targetId ? 'block' : 'none'; }); // Update URL hash window.location.hash = targetId; // Load section-specific data if (targetId === 'walk-sheet') { checkAndLoadWalkSheetConfig(); } else if (targetId === 'dashboard') { loadDashboardData(); } else if (targetId === 'shifts') { loadAdminShifts(); } else if (targetId === 'users') { loadUsers(); } else if (targetId === 'convert-data') { // Initialize data convert event listeners when section is shown setTimeout(() => { if (typeof window.setupDataConvertEventListeners === 'function') { console.log('Setting up data convert event listeners...'); window.setupDataConvertEventListeners(); } else { console.error('setupDataConvertEventListeners not found'); } }, 100); } // Close mobile menu if open const sidebar = document.getElementById('admin-sidebar'); if (sidebar && sidebar.classList.contains('open')) { sidebar.classList.remove('open'); } }); }); // Set initial active state based on current hash or default const currentHash = window.location.hash || '#dashboard'; const activeLink = document.querySelector(`.admin-nav a[href="${currentHash}"]`); if (activeLink) { activeLink.classList.add('active'); } // Also check if we're already on the shifts page (via hash) const hash = window.location.hash; if (hash === '#shifts') { showSection('shifts'); loadAdminShifts(); } } // Helper function to show a specific section function showSection(sectionId) { const sections = document.querySelectorAll('.admin-section'); const navLinks = document.querySelectorAll('.admin-nav a'); // Hide all sections sections.forEach(section => { section.style.display = section.id === sectionId ? 'block' : 'none'; }); // Update active nav navLinks.forEach(link => { const linkTarget = link.getAttribute('href').substring(1); link.classList.toggle('active', linkTarget === sectionId); }); // Special handling for convert-data section if (sectionId === 'convert-data') { // Initialize data convert event listeners when section is shown setTimeout(() => { if (typeof window.setupDataConvertEventListeners === 'function') { console.log('Setting up data convert event listeners from showSection...'); window.setupDataConvertEventListeners(); } else { console.error('setupDataConvertEventListeners not found in showSection'); } }, 100); } // Special handling for cuts section if (sectionId === 'cuts') { // Initialize admin cuts manager when section is shown setTimeout(() => { if (typeof window.adminCutsManager === 'object' && window.adminCutsManager.initialize) { if (!window.adminCutsManager.isInitialized) { console.log('Initializing admin cuts manager from showSection...'); window.adminCutsManager.initialize().catch(error => { console.error('Failed to initialize cuts manager:', error); }); } else { console.log('Admin cuts manager already initialized'); } } else { console.error('adminCutsManager not found in showSection'); } }, 100); } // Special handling for shifts section if (sectionId === 'shifts') { console.log('Loading shifts for admin panel...'); loadAdminShifts(); } } // Update map from input fields function updateMapFromInputs() { const lat = parseFloat(document.getElementById('start-lat').value); const lng = parseFloat(document.getElementById('start-lng').value); const zoom = parseInt(document.getElementById('start-zoom').value); if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) { adminMap.setView([lat, lng], zoom); updateStartMarker(lat, lng); } } // Save start location async function saveStartLocation() { const lat = parseFloat(document.getElementById('start-lat').value); const lng = parseFloat(document.getElementById('start-lng').value); const zoom = parseInt(document.getElementById('start-zoom').value); // Validate if (isNaN(lat) || isNaN(lng) || isNaN(zoom)) { showStatus('Please enter valid coordinates and zoom level', 'error'); return; } if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { showStatus('Coordinates out of valid range', 'error'); return; } if (zoom < 2 || zoom > 19) { showStatus('Zoom level must be between 2 and 19', 'error'); return; } try { const response = await fetch('/api/admin/start-location', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ latitude: lat, longitude: lng, zoom: zoom }) }); const data = await response.json(); if (data.success) { showStatus('Start location saved successfully!', 'success'); } else { throw new Error(data.error || 'Failed to save'); } } catch (error) { console.error('Save error:', error); showStatus(error.message || 'Failed to save start location', 'error'); } } // Save walk sheet configuration async function saveWalkSheetConfig() { const config = { walk_sheet_title: document.getElementById('walk-sheet-title')?.value || '', walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle')?.value || '', walk_sheet_footer: document.getElementById('walk-sheet-footer')?.value || '', qr_code_1_url: document.getElementById('qr-code-1-url')?.value || '', qr_code_1_label: document.getElementById('qr-code-1-label')?.value || '', qr_code_2_url: document.getElementById('qr-code-2-url')?.value || '', qr_code_2_label: document.getElementById('qr-code-2-label')?.value || '', qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '', qr_code_3_label: document.getElementById('qr-code-3-label')?.value || '' }; console.log('Saving walk sheet config:', config); // Show loading state const saveButton = document.getElementById('save-walk-sheet'); if (!saveButton) { showStatus('Save button not found', 'error'); return; } const originalText = saveButton.textContent; saveButton.textContent = 'Saving...'; saveButton.disabled = true; try { const response = await fetch('/api/admin/walk-sheet-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const data = await response.json(); console.log('Save response:', data); if (data.success) { showStatus('Walk sheet configuration saved successfully!', 'success'); console.log('Configuration saved successfully'); // Don't reload config here - the form already has the latest values // Just regenerate the preview generateWalkSheetPreview(); } else { throw new Error(data.error || 'Failed to save'); } } catch (error) { console.error('Save error:', error); showStatus(error.message || 'Failed to save walk sheet configuration', 'error'); } finally { saveButton.textContent = originalText; saveButton.disabled = false; } } // Generate walk sheet preview function generateWalkSheetPreview() { const title = document.getElementById('walk-sheet-title')?.value || 'Campaign Walk Sheet'; const subtitle = document.getElementById('walk-sheet-subtitle')?.value || 'Door-to-Door Canvassing Form'; const footer = document.getElementById('walk-sheet-footer')?.value || 'Thank you for your support!'; let previewHTML = `

${escapeHtml(title)}

${escapeHtml(subtitle)}

`; // Add QR codes section const qrCodesHTML = []; for (let i = 1; i <= 3; i++) { const urlInput = document.getElementById(`qr-code-${i}-url`); const labelInput = document.getElementById(`qr-code-${i}-label`); const url = urlInput?.value || ''; const label = labelInput?.value || ''; if (url) { qrCodesHTML.push(`
${escapeHtml(label || `QR Code ${i}`)}
`); } } if (qrCodesHTML.length > 0) { previewHTML += `
${qrCodesHTML.join('')}
`; } // Add form fields based on the main map form previewHTML += `
1 2 3 4
Y Yes N No
R L U
Notes & Comments
`; // Add footer if (footer) { previewHTML += ` `; } // Update preview const previewContent = document.getElementById('walk-sheet-preview-content'); if (previewContent) { previewContent.innerHTML = previewHTML; // Generate QR codes after DOM is updated setTimeout(() => { generatePreviewQRCodes(); }, 100); } else { console.warn('Walk sheet preview content container not found'); } } // Update the generatePreviewQRCodes function to use smaller size async function generatePreviewQRCodes() { for (let i = 1; i <= 3; i++) { const urlInput = document.getElementById(`qr-code-${i}-url`); const url = urlInput?.value || ''; const qrContainer = document.getElementById(`preview-qr-${i}`); if (url && qrContainer) { try { // Use our local QR code generation endpoint with size matching display const qrImageUrl = `/api/qr?text=${encodeURIComponent(url)}&size=200`; // Generate at higher res qrContainer.innerHTML = `QR Code ${i}`; // Display smaller } catch (error) { console.error(`Failed to display QR code ${i}:`, error); qrContainer.innerHTML = '
QR Error
'; } } else if (qrContainer) { // Clear empty QR containers qrContainer.innerHTML = ''; } } } // Print walk sheet function printWalkSheet() { // First generate fresh preview to ensure QR codes are generated generateWalkSheetPreview(); // Wait for QR codes to generate, then print setTimeout(() => { const previewContent = document.getElementById('walk-sheet-preview-content'); const clonedContent = previewContent.cloneNode(true); // Convert canvas elements to images for printing const canvases = previewContent.querySelectorAll('canvas'); const clonedCanvases = clonedContent.querySelectorAll('canvas'); canvases.forEach((canvas, index) => { if (canvas && clonedCanvases[index]) { const img = document.createElement('img'); img.src = canvas.toDataURL('image/png'); img.width = canvas.width; img.height = canvas.height; img.style.width = canvas.style.width || `${canvas.width}px`; img.style.height = canvas.style.height || `${canvas.height}px`; clonedCanvases[index].parentNode.replaceChild(img, clonedCanvases[index]); } }); // Create a print-specific window const printWindow = window.open('', '_blank'); printWindow.document.write(` Walk Sheet - Print
${clonedContent.innerHTML}
`); printWindow.document.close(); printWindow.onload = function() { setTimeout(() => { printWindow.print(); // User can close manually after printing }, 500); }; }, 1000); // Give QR codes time to generate } // Load walk sheet configuration async function loadWalkSheetConfig() { try { console.log('Loading walk sheet config...'); const response = await fetch('/api/admin/walk-sheet-config'); const data = await response.json(); console.log('Loaded walk sheet config:', data); if (data.success) { // The config object contains the actual configuration const config = data.config || {}; console.log('Config object:', config); // Populate form fields - use the exact field names from the backend const titleInput = document.getElementById('walk-sheet-title'); const subtitleInput = document.getElementById('walk-sheet-subtitle'); const footerInput = document.getElementById('walk-sheet-footer'); console.log('Found form elements:', { title: !!titleInput, subtitle: !!subtitleInput, footer: !!footerInput }); if (titleInput) { titleInput.value = config.walk_sheet_title || 'Campaign Walk Sheet'; console.log('Set title to:', titleInput.value); } if (subtitleInput) { subtitleInput.value = config.walk_sheet_subtitle || 'Door-to-Door Canvassing Form'; console.log('Set subtitle to:', subtitleInput.value); } if (footerInput) { footerInput.value = config.walk_sheet_footer || 'Thank you for your support!'; console.log('Set footer to:', footerInput.value); } // Populate QR code fields for (let i = 1; i <= 3; i++) { const urlField = document.getElementById(`qr-code-${i}-url`); const labelField = document.getElementById(`qr-code-${i}-label`); console.log(`QR ${i} fields found:`, { url: !!urlField, label: !!labelField }); if (urlField) { urlField.value = config[`qr_code_${i}_url`] || ''; console.log(`Set QR ${i} URL to:`, urlField.value); } if (labelField) { labelField.value = config[`qr_code_${i}_label`] || ''; console.log(`Set QR ${i} label to:`, labelField.value); } } console.log('Walk sheet config loaded successfully'); // Show status message about data source if (data.source) { const sourceText = data.source === 'database' ? 'Walk sheet config loaded from database' : data.source === 'defaults' ? 'Using walk sheet defaults' : 'Walk sheet config loaded'; showStatus(sourceText, 'info'); } return true; } else { console.error('Failed to load config:', data.error); showStatus('Failed to load walk sheet configuration', 'error'); return false; } } catch (error) { console.error('Failed to load walk sheet config:', error); showStatus('Failed to load walk sheet configuration', 'error'); return false; } } // Check if walk sheet section is visible and load config if needed function checkAndLoadWalkSheetConfig() { const walkSheetSection = document.getElementById('walk-sheet'); if (walkSheetSection && walkSheetSection.style.display !== 'none') { console.log('Walk sheet section is visible, loading config...'); loadWalkSheetConfig().then((success) => { if (success) { generateWalkSheetPreview(); } }); } } // Add a function to force load config when walk sheet section is accessed function showWalkSheetSection() { const walkSheetSection = document.getElementById('walk-sheet'); const startLocationSection = document.getElementById('start-location'); if (startLocationSection) { startLocationSection.style.display = 'none'; } if (walkSheetSection) { walkSheetSection.style.display = 'block'; // Load config after section is shown setTimeout(() => { loadWalkSheetConfig().then((success) => { if (success) { generateWalkSheetPreview(); } }); }, 100); // Small delay to ensure DOM is ready } } // Add event listener to trigger config load when walking sheet nav is clicked document.addEventListener('DOMContentLoaded', function() { // Add additional event listener for walk sheet nav const walkSheetNav = document.querySelector('.admin-nav a[href="#walk-sheet"]'); if (walkSheetNav) { walkSheetNav.addEventListener('click', function(e) { e.preventDefault(); showWalkSheetSection(); // Update nav state document.querySelectorAll('.admin-nav a').forEach(link => { link.classList.remove('active'); }); this.classList.add('active'); }); } }); // Handle logout async function handleLogout() { if (!confirm('Are you sure you want to logout?')) { return; } try { const response = await fetch('/api/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { window.location.href = '/login.html'; } else { showStatus('Logout failed. Please try again.', 'error'); } } catch (error) { console.error('Logout error:', error); showStatus('Logout failed. Please try again.', 'error'); } } // Show status message function showStatus(message, type = 'info') { let container = document.getElementById('status-container'); // Create container if it doesn't exist if (!container) { container = document.createElement('div'); container.id = 'status-container'; container.className = 'status-container'; // Ensure proper z-index even if CSS hasn't loaded container.style.zIndex = '12300'; document.body.appendChild(container); } const messageDiv = document.createElement('div'); messageDiv.className = `status-message ${type}`; messageDiv.textContent = message; // Add click to dismiss functionality messageDiv.addEventListener('click', () => { messageDiv.remove(); }); // Add a small close button for better UX const closeBtn = document.createElement('span'); closeBtn.innerHTML = ' ×'; closeBtn.style.float = 'right'; closeBtn.style.fontWeight = 'bold'; closeBtn.style.marginLeft = '10px'; closeBtn.style.cursor = 'pointer'; closeBtn.setAttribute('title', 'Click to dismiss'); messageDiv.appendChild(closeBtn); container.appendChild(messageDiv); // Auto-remove after 5 seconds setTimeout(() => { if (messageDiv.parentNode) { messageDiv.remove(); } }, 5000); } // Escape HTML function escapeHtml(text) { if (text === null || text === undefined) { return ''; } const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Debounce function for input events function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 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; 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'}

`; }).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); } }); } // 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) function editShift(shiftId) { showStatus('Edit functionality coming soon', 'info'); } // Add function to create shift async function createShift(e) { e.preventDefault(); const formData = { title: document.getElementById('shift-title').value, description: document.getElementById('shift-description').value, date: document.getElementById('shift-date').value, startTime: document.getElementById('shift-start').value, endTime: document.getElementById('shift-end').value, location: document.getElementById('shift-location').value, maxVolunteers: document.getElementById('shift-max-volunteers').value }; try { const response = await fetch('/api/shifts/admin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const data = await response.json(); if (data.success) { showStatus('Shift created successfully', 'success'); document.getElementById('shift-form').reset(); await loadAdminShifts(); console.log('Refreshed shifts list after creating new shift'); } else { showStatus(data.error || 'Failed to create shift', 'error'); } } catch (error) { console.error('Error creating shift:', error); showStatus('Failed to create shift', 'error'); } } function clearShiftForm() { const form = document.getElementById('shift-form'); if (form) { form.reset(); 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) { showStatus('No shift selected', 'error'); 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 await refreshCurrentShiftData(); console.log('Refreshed shift data after adding user'); } 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) { showStatus('No shift selected', 'error'); 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 await refreshCurrentShiftData(); console.log('Refreshed shift data after removing volunteer'); } 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) return; try { console.log('Refreshing shift data for shift ID:', currentShiftData.ID); // Reload admin shifts to get updated data const response = await fetch('/api/shifts/admin'); const data = await response.json(); if (data.success) { const updatedShift = data.shifts.find(s => s.ID === currentShiftData.ID); if (updatedShift) { console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers'); currentShiftData = updatedShift; displayCurrentVolunteers(updatedShift.signups || []); // Immediately refresh the main shifts list to show updated counts console.log('Refreshing main shifts list with', data.shifts.length, 'shifts'); displayAdminShifts(data.shifts); } else { console.warn('Could not find updated shift with ID:', currentShiftData.ID); } } else { console.error('Failed to refresh shift data:', data.error); } } catch (error) { console.error('Error refreshing shift data:', error); } } // Close modal function closeShiftUserModal() { document.getElementById('shift-user-modal').style.display = 'none'; currentShiftData = null; // Refresh the main shifts list one more time when closing the modal // to ensure any changes are reflected console.log('Refreshing shifts list on modal close'); loadAdminShifts(); } // 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(); });