// Global configuration const CONFIG = { DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461, DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938, DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11, REFRESH_INTERVAL: 30000, // 30 seconds MAX_ZOOM: 19, MIN_ZOOM: 2 }; // Application state let map = null; let markers = []; let userLocationMarker = null; let isAddingLocation = false; let refreshInterval = null; let currentEditingLocation = null; let currentUser = null; // Add current user state // Initialize application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { initializeMap(); checkAuthentication(); // Add authentication check loadLocations(); setupEventListeners(); checkConfiguration(); // Set up auto-refresh refreshInterval = setInterval(loadLocations, CONFIG.REFRESH_INTERVAL); // Add event delegation for dynamically created edit buttons document.addEventListener('click', function(e) { if (e.target.classList.contains('edit-location-btn')) { const locationId = e.target.getAttribute('data-location-id'); editLocation(locationId); } }); }); // Initialize Leaflet map function initializeMap() { // Create map instance map = L.map('map', { center: [CONFIG.DEFAULT_LAT, CONFIG.DEFAULT_LNG], zoom: CONFIG.DEFAULT_ZOOM, zoomControl: true, attributionControl: true }); // Add OpenStreetMap tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: CONFIG.MAX_ZOOM, minZoom: CONFIG.MIN_ZOOM }).addTo(map); // Add scale control L.control.scale({ position: 'bottomleft', metric: true, imperial: false }).addTo(map); // Hide loading overlay document.getElementById('loading').classList.add('hidden'); } // Set up event listeners function setupEventListeners() { // Geolocation button document.getElementById('geolocate-btn').addEventListener('click', handleGeolocation); // Add location button document.getElementById('add-location-btn').addEventListener('click', toggleAddLocation); // Refresh button document.getElementById('refresh-btn').addEventListener('click', () => { showStatus('Refreshing locations...', 'info'); loadLocations(); }); // Fullscreen button document.getElementById('fullscreen-btn').addEventListener('click', toggleFullscreen); // Form submission document.getElementById('location-form').addEventListener('submit', handleLocationSubmit); // Edit form submission document.getElementById('edit-location-form').addEventListener('submit', handleEditLocationSubmit); // Map click handler for adding locations map.on('click', handleMapClick); // Set up geo field synchronization setupGeoFieldSync(); // Add event listeners for buttons that were using inline onclick document.getElementById('close-edit-footer-btn').addEventListener('click', closeEditFooter); document.getElementById('lookup-address-edit-btn').addEventListener('click', lookupAddressForEdit); document.getElementById('delete-location-btn').addEventListener('click', deleteLocation); document.getElementById('close-modal-btn').addEventListener('click', closeModal); document.getElementById('lookup-address-add-btn').addEventListener('click', lookupAddressForAdd); document.getElementById('cancel-modal-btn').addEventListener('click', closeModal); } // Helper function to get color based on support level function getSupportColor(supportLevel) { const level = parseInt(supportLevel); switch(level) { case 1: return '#27ae60'; // Green - Strong support case 2: return '#f1c40f'; // Yellow - Moderate support case 3: return '#e67e22'; // Orange - Low support case 4: return '#e74c3c'; // Red - No support default: return '#95a5a6'; // Grey - Unknown/null } } // Helper function to get support level text function getSupportLevelText(level) { const levelNum = parseInt(level); switch(levelNum) { case 1: return '1 - Strong Support'; case 2: return '2 - Moderate Support'; case 3: return '3 - Low Support'; case 4: return '4 - No Support'; default: return 'Not Specified'; } } // Set up geo field synchronization function setupGeoFieldSync() { const latInput = document.getElementById('location-lat'); const lngInput = document.getElementById('location-lng'); const geoLocationInput = document.getElementById('geo-location'); // Validate geo-location format function validateGeoLocation(value) { if (!value) return false; // Check both formats const patterns = [ /^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/, // comma-separated /^-?\d+\.?\d*\s*;\s*-?\d+\.?\d*$/ // semicolon-separated ]; return patterns.some(pattern => pattern.test(value)); } // When lat/lng change, update geo-location function updateGeoLocation() { const lat = parseFloat(latInput.value); const lng = parseFloat(lngInput.value); if (!isNaN(lat) && !isNaN(lng)) { geoLocationInput.value = `${lat};${lng}`; // Use semicolon format for NocoDB geoLocationInput.classList.remove('invalid'); geoLocationInput.classList.add('valid'); } } // When geo-location changes, parse and update lat/lng function parseGeoLocation() { const geoValue = geoLocationInput.value.trim(); if (!geoValue) { geoLocationInput.classList.remove('valid', 'invalid'); return; } if (!validateGeoLocation(geoValue)) { geoLocationInput.classList.add('invalid'); geoLocationInput.classList.remove('valid'); return; } // Try semicolon-separated first let parts = geoValue.split(';'); if (parts.length === 2) { const lat = parseFloat(parts[0].trim()); const lng = parseFloat(parts[1].trim()); if (!isNaN(lat) && !isNaN(lng)) { latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); // Keep semicolon format for NocoDB GeoData geoLocationInput.value = `${lat};${lng}`; geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); return; } } // Try comma-separated parts = geoValue.split(','); if (parts.length === 2) { const lat = parseFloat(parts[0].trim()); const lng = parseFloat(parts[1].trim()); if (!isNaN(lat) && !isNaN(lng)) { latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); // Normalize to semicolon format for NocoDB GeoData geoLocationInput.value = `${lat};${lng}`; geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); } } } // Add event listeners latInput.addEventListener('input', updateGeoLocation); lngInput.addEventListener('input', updateGeoLocation); geoLocationInput.addEventListener('blur', parseGeoLocation); geoLocationInput.addEventListener('input', () => { // Clear validation classes on input to allow real-time feedback const geoValue = geoLocationInput.value.trim(); if (geoValue && validateGeoLocation(geoValue)) { geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); } else if (geoValue) { geoLocationInput.classList.add('invalid'); geoLocationInput.classList.remove('valid'); } else { geoLocationInput.classList.remove('valid', 'invalid'); } }); } // Check authentication and display user info async function checkAuthentication() { try { const response = await fetch('/api/auth/check'); const data = await response.json(); if (data.authenticated && data.user) { currentUser = data.user; displayUserInfo(); } } catch (error) { console.error('Failed to check authentication:', error); } } // Display user info in header function displayUserInfo() { const headerActions = document.querySelector('.header-actions'); // Create user info element const userInfo = document.createElement('div'); userInfo.className = 'user-info'; userInfo.innerHTML = ` ${escapeHtml(currentUser.email)} `; // Insert before the location count const locationCount = document.getElementById('location-count'); headerActions.insertBefore(userInfo, locationCount); // Add logout event listener document.getElementById('logout-btn').addEventListener('click', handleLogout); } // 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'); } } // Check API configuration async function checkConfiguration() { try { const response = await fetch('/api/config-check'); const data = await response.json(); if (!data.configured) { showStatus('Warning: API not fully configured. Check your .env file.', 'warning'); } } catch (error) { console.error('Configuration check failed:', error); } } // Load locations from API async function loadLocations() { try { const response = await fetch('/api/locations'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.success) { displayLocations(data.locations); updateLocationCount(data.count); } else { throw new Error(data.error || 'Failed to load locations'); } } catch (error) { console.error('Error loading locations:', error); showStatus('Failed to load locations. Check your connection.', 'error'); updateLocationCount(0); } } // Display locations on map function displayLocations(locations) { // Clear existing markers markers.forEach(marker => map.removeLayer(marker)); markers = []; // Add new markers locations.forEach(location => { if (location.latitude && location.longitude) { const marker = createLocationMarker(location); markers.push(marker); } }); // Fit map to show all markers if there are any if (markers.length > 0) { const group = new L.featureGroup(markers); map.fitBounds(group.getBounds().pad(0.1)); } } // Create marker for location (updated to use circle markers) function createLocationMarker(location) { console.log('Creating marker for location:', location); // Get color based on support level const supportColor = getSupportColor(location['Support Level']); // Create circle marker instead of default marker const marker = L.circleMarker([location.latitude, location.longitude], { radius: 8, fillColor: supportColor, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.8, title: location.title || 'Location', riseOnHover: true, locationData: location // Store location data in marker options }).addTo(map); // Add larger radius on hover marker.on('mouseover', function() { this.setRadius(10); }); marker.on('mouseout', function() { this.setRadius(8); }); // Create popup content const popupContent = createPopupContent(location); marker.bindPopup(popupContent); return marker; } // Create popup content for marker function createPopupContent(location) { console.log('Creating popup for location:', location); let content = ''; return content; } // Handle geolocation function handleGeolocation() { if (!navigator.geolocation) { showStatus('Geolocation is not supported by your browser', 'error'); return; } showStatus('Getting your location...', 'info'); navigator.geolocation.getCurrentPosition( (position) => { const { latitude, longitude, accuracy } = position.coords; // Center map on user location map.setView([latitude, longitude], 15); // Remove existing user marker if (userLocationMarker) { map.removeLayer(userLocationMarker); } // Add user location marker userLocationMarker = L.marker([latitude, longitude], { icon: L.divIcon({ html: '
', className: 'user-location-marker', iconSize: [20, 20], iconAnchor: [10, 10] }), title: 'Your location' }).addTo(map); // Add accuracy circle L.circle([latitude, longitude], { radius: accuracy, color: '#2c5aa0', fillColor: '#2c5aa0', fillOpacity: 0.1, weight: 1 }).addTo(map); showStatus(`Location found (±${Math.round(accuracy)}m accuracy)`, 'success'); }, (error) => { let message = 'Unable to get your location'; switch (error.code) { case error.PERMISSION_DENIED: message = 'Location permission denied'; break; case error.POSITION_UNAVAILABLE: message = 'Location information unavailable'; break; case error.TIMEOUT: message = 'Location request timed out'; break; } showStatus(message, 'error'); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); } // Toggle add location mode function toggleAddLocation() { isAddingLocation = !isAddingLocation; const btn = document.getElementById('add-location-btn'); const crosshair = document.getElementById('crosshair'); if (isAddingLocation) { btn.innerHTML = 'Cancel'; btn.classList.remove('btn-success'); btn.classList.add('btn-secondary'); crosshair.classList.remove('hidden'); map.getContainer().style.cursor = 'crosshair'; } else { btn.innerHTML = 'Add Location Here'; btn.classList.remove('btn-secondary'); btn.classList.add('btn-success'); crosshair.classList.add('hidden'); map.getContainer().style.cursor = ''; } } // Handle map click function handleMapClick(e) { if (!isAddingLocation) return; const { lat, lng } = e.latlng; // Toggle off add location mode toggleAddLocation(); // Show modal with coordinates showAddLocationModal(lat, lng); } // Show add location modal function showAddLocationModal(lat, lng) { const modal = document.getElementById('add-modal'); const latInput = document.getElementById('location-lat'); const lngInput = document.getElementById('location-lng'); const geoLocationInput = document.getElementById('geo-location'); // Set coordinates latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); // Set geo-location field geoLocationInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; // Use semicolon format for NocoDB geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); // Clear other fields document.getElementById('first-name').value = ''; document.getElementById('last-name').value = ''; document.getElementById('location-email').value = ''; document.getElementById('location-unit').value = ''; document.getElementById('support-level').value = ''; const addressInput = document.getElementById('location-address'); addressInput.value = 'Looking up address...'; // Show loading message document.getElementById('sign').checked = false; document.getElementById('sign-size').value = ''; // Show modal modal.classList.remove('hidden'); // Fetch address asynchronously reverseGeocode(lat, lng).then(result => { if (result) { addressInput.value = result.formattedAddress || result.fullAddress; } else { addressInput.value = ''; // Clear if lookup fails // Don't show warning for automatic lookups } }).catch(error => { console.error('Address lookup failed:', error); addressInput.value = ''; }); // Focus on first name input setTimeout(() => { document.getElementById('first-name').focus(); }, 100); } // Close modal function closeModal() { document.getElementById('add-modal').classList.add('hidden'); } // Edit location function function editLocation(locationId) { // Find the location in markers data const location = markers.find(m => { const data = m.options.locationData; return String(data.id || data.Id) === String(locationId); })?.options.locationData; if (!location) { console.error('Location not found for ID:', locationId); console.log('Available locations:', markers.map(m => ({ id: m.options.locationData.id || m.options.locationData.Id, name: m.options.locationData['First Name'] + ' ' + m.options.locationData['Last Name'] }))); showStatus('Location not found', 'error'); return; } currentEditingLocation = location; // Populate all the edit form fields document.getElementById('edit-location-id').value = location.id || location.Id || ''; document.getElementById('edit-first-name').value = location['First Name'] || ''; document.getElementById('edit-last-name').value = location['Last Name'] || ''; document.getElementById('edit-location-email').value = location['Email'] || ''; document.getElementById('edit-location-unit').value = location['Unit Number'] || ''; document.getElementById('edit-support-level').value = location['Support Level'] || ''; const addressInput = document.getElementById('edit-location-address'); addressInput.value = location['Address'] || ''; // If no address exists, try to fetch it if (!location['Address'] && location.latitude && location.longitude) { addressInput.value = 'Looking up address...'; reverseGeocode(location.latitude, location.longitude).then(result => { if (result && !location['Address']) { addressInput.value = result.formattedAddress || result.fullAddress; } else if (!location['Address']) { addressInput.value = ''; // Don't show error - just silently fail } }).catch(error => { // Handle any unexpected errors console.error('Address lookup failed:', error); addressInput.value = ''; }); } // Handle checkbox document.getElementById('edit-sign').checked = location['Sign'] === true || location['Sign'] === 'true' || location['Sign'] === 1; document.getElementById('edit-sign-size').value = location['Sign Size'] || ''; document.getElementById('edit-location-lat').value = location.latitude || ''; document.getElementById('edit-location-lng').value = location.longitude || ''; document.getElementById('edit-geo-location').value = location['Geo-Location'] || `${location.latitude};${location.longitude}`; // Show the edit footer document.getElementById('edit-footer').classList.remove('hidden'); document.getElementById('map-container').classList.add('edit-mode'); // Invalidate map size after showing footer setTimeout(() => map.invalidateSize(), 300); // Setup geo field sync for edit form setupEditGeoFieldSync(); } // Close edit footer function closeEditFooter() { document.getElementById('edit-footer').classList.add('hidden'); document.getElementById('map-container').classList.remove('edit-mode'); currentEditingLocation = null; // Invalidate map size after hiding footer setTimeout(() => map.invalidateSize(), 300); } // Setup geo field sync for edit form function setupEditGeoFieldSync() { const latInput = document.getElementById('edit-location-lat'); const lngInput = document.getElementById('edit-location-lng'); const geoLocationInput = document.getElementById('edit-geo-location'); // Similar to setupGeoFieldSync but for edit form function updateGeoLocation() { const lat = parseFloat(latInput.value); const lng = parseFloat(lngInput.value); if (!isNaN(lat) && !isNaN(lng)) { geoLocationInput.value = `${lat};${lng}`; geoLocationInput.classList.remove('invalid'); geoLocationInput.classList.add('valid'); } } function parseGeoLocation() { const geoValue = geoLocationInput.value.trim(); if (!geoValue) { geoLocationInput.classList.remove('valid', 'invalid'); return; } // Try semicolon-separated first let parts = geoValue.split(';'); if (parts.length === 2) { const lat = parseFloat(parts[0].trim()); const lng = parseFloat(parts[1].trim()); if (!isNaN(lat) && !isNaN(lng)) { latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); return; } } // Try comma-separated parts = geoValue.split(','); if (parts.length === 2) { const lat = parseFloat(parts[0].trim()); const lng = parseFloat(parts[1].trim()); if (!isNaN(lat) && !isNaN(lng)) { latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); geoLocationInput.value = `${lat};${lng}`; geoLocationInput.classList.add('valid'); geoLocationInput.classList.remove('invalid'); } } } latInput.addEventListener('input', updateGeoLocation); lngInput.addEventListener('input', updateGeoLocation); geoLocationInput.addEventListener('blur', parseGeoLocation); } // Handle edit form submission async function handleEditLocationSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); const locationId = data.id; // Ensure Geo-Location field is included const geoLocationInput = document.getElementById('edit-geo-location'); if (geoLocationInput.value) { data['Geo-Location'] = geoLocationInput.value; } try { const response = await fetch(`/api/locations/${locationId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (response.ok && result.success) { showStatus('Location updated successfully!', 'success'); closeEditFooter(); // Reload locations loadLocations(); } else { throw new Error(result.error || 'Failed to update location'); } } catch (error) { console.error('Error updating location:', error); showStatus(error.message, 'error'); } } // Delete location async function deleteLocation() { if (!currentEditingLocation) return; const locationId = currentEditingLocation.id || currentEditingLocation.Id; if (!confirm('Are you sure you want to delete this location?')) { return; } try { const response = await fetch(`/api/locations/${locationId}`, { method: 'DELETE' }); const result = await response.json(); if (response.ok && result.success) { showStatus('Location deleted successfully!', 'success'); closeEditFooter(); // Reload locations loadLocations(); } else { throw new Error(result.error || 'Failed to delete location'); } } catch (error) { console.error('Error deleting location:', error); showStatus(error.message, 'error'); } } // Handle location form submission async function handleLocationSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); // Validate required fields - either first name or last name should be provided if ((!data['First Name'] || !data['First Name'].trim()) && (!data['Last Name'] || !data['Last Name'].trim())) { showStatus('Either First Name or Last Name is required', 'error'); return; } // Ensure Geo-Location field is included const geoLocationInput = document.getElementById('geo-location'); if (geoLocationInput.value) { data['Geo-Location'] = geoLocationInput.value; } try { const response = await fetch('/api/locations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (response.ok && result.success) { showStatus('Location added successfully!', 'success'); closeModal(); // Reload locations loadLocations(); // Center map on new location map.setView([data.latitude, data.longitude], map.getZoom()); } else { throw new Error(result.error || 'Failed to add location'); } } catch (error) { console.error('Error adding location:', error); showStatus(error.message, 'error'); } } // Toggle fullscreen function toggleFullscreen() { const app = document.getElementById('app'); const btn = document.getElementById('fullscreen-btn'); if (!document.fullscreenElement) { app.requestFullscreen().then(() => { app.classList.add('fullscreen'); btn.innerHTML = 'Exit Fullscreen'; // Invalidate map size after transition setTimeout(() => map.invalidateSize(), 300); }).catch(err => { showStatus('Unable to enter fullscreen', 'error'); }); } else { document.exitFullscreen().then(() => { app.classList.remove('fullscreen'); btn.innerHTML = 'Fullscreen'; // Invalidate map size after transition setTimeout(() => map.invalidateSize(), 300); }); } } // Update location count function updateLocationCount(count) { const countElement = document.getElementById('location-count'); countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`; } // Show status message function showStatus(message, type = 'info') { const container = document.getElementById('status-container'); const messageDiv = document.createElement('div'); messageDiv.className = `status-message ${type}`; messageDiv.textContent = message; container.appendChild(messageDiv); // Auto-remove after 5 seconds setTimeout(() => { messageDiv.remove(); }, 5000); } // Escape HTML to prevent XSS function escapeHtml(text) { if (text === null || text === undefined) { return ''; } const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Handle window resize window.addEventListener('resize', () => { map.invalidateSize(); }); // Clean up on page unload window.addEventListener('beforeunload', () => { if (refreshInterval) { clearInterval(refreshInterval); } }); // Reverse geocode to get address from coordinates async function reverseGeocode(lat, lng) { try { const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Geocoding service unavailable'); } const result = await response.json(); if (!result.success || !result.data) { throw new Error('Geocoding failed'); } return result.data; } catch (error) { console.error('Reverse geocoding error:', error); return null; } } // Add a new function for forward geocoding (address to coordinates) async function forwardGeocode(address) { try { const response = await fetch(`/api/geocode/forward?address=${encodeURIComponent(address)}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Geocoding service unavailable'); } const result = await response.json(); if (!result.success || !result.data) { throw new Error('Geocoding failed'); } return result.data; } catch (error) { console.error('Forward geocoding error:', error); return null; } } // Manual address lookup for add form async function lookupAddressForAdd() { const latInput = document.getElementById('location-lat'); const lngInput = document.getElementById('location-lng'); const addressInput = document.getElementById('location-address'); const lat = parseFloat(latInput.value); const lng = parseFloat(lngInput.value); if (!isNaN(lat) && !isNaN(lng)) { addressInput.value = 'Looking up address...'; const result = await reverseGeocode(lat, lng); if (result) { addressInput.value = result.formattedAddress || result.fullAddress; showStatus('Address found!', 'success'); } else { addressInput.value = ''; showStatus('Could not find address for these coordinates', 'warning'); } } else { showStatus('Please enter valid coordinates first', 'warning'); } } // Manual address lookup for edit form async function lookupAddressForEdit() { const latInput = document.getElementById('edit-location-lat'); const lngInput = document.getElementById('edit-location-lng'); const addressInput = document.getElementById('edit-location-address'); const lat = parseFloat(latInput.value); const lng = parseFloat(lngInput.value); if (!isNaN(lat) && !isNaN(lng)) { addressInput.value = 'Looking up address...'; const result = await reverseGeocode(lat, lng); if (result) { addressInput.value = result.formattedAddress || result.fullAddress; showStatus('Address found!', 'success'); } else { addressInput.value = ''; showStatus('Could not find address for these coordinates', 'warning'); } } else { showStatus('Please enter valid coordinates first', 'warning'); } }