// Admin panel JavaScript let adminMap = null; let startMarker = null; let storedQRCodes = {}; // 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(); }, 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 { // 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); } } // 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); } } // 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() { try { const response = await fetch('/api/shifts/admin'); const data = await response.json(); if (data.success) { displayAdminShifts(data.shifts); } else { showStatus('Failed to load shifts', 'error'); } } catch (error) { console.error('Error loading admin shifts:', error); 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 = new Date(shift.Date); const signupCount = shift.signups ? shift.signups.length : 0; 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); } }); } // 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(); } 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(); } 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; if (!users || users.length === 0) { container.innerHTML = '

Existing Users

No users found.

'; return; } const tableHtml = `

Existing Users

${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 userId = user.Id || user.id || user.ID; return ` `; }).join('')}
Email Name Role Created Actions
${escapeHtml(user.email || user.Email || 'N/A')} ${escapeHtml(user.name || user.Name || 'N/A')} ${isAdmin ? 'Admin' : 'User'} ${formattedDate}
`; container.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); } }); } 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 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; } try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, name: name || '', admin }) }); 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(); showStatus('User form cleared', 'info'); } } // 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}`); } }