// 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; let startLocationMarker = null; let isStartLocationVisible = true; // Initialize the application document.addEventListener('DOMContentLoaded', async () => { console.log('DOM loaded, initializing application...'); try { // First check authentication await checkAuth(); // Then initialize the map await initializeMap(); // Only load locations after map is ready await loadLocations(); // Setup other features setupEventListeners(); setupAutoRefresh(); } catch (error) { console.error('Initialization error:', error); showStatus('Failed to initialize application', 'error'); } finally { hideLoading(); } }); // Check authentication async function checkAuth() { try { const response = await fetch('/api/auth/check'); const data = await response.json(); if (!data.authenticated) { window.location.href = '/login.html'; // Throw error to stop further initialization throw new Error('Not authenticated'); } currentUser = data.user; updateUserInterface(); } catch (error) { console.error('Auth check failed:', error); window.location.href = '/login.html'; // Re-throw to stop initialization chain throw error; } } // Update UI based on user function updateUserInterface() { if (!currentUser) return; // Update user email in both desktop and mobile const userEmailElement = document.getElementById('user-email'); const mobileUserEmailElement = document.getElementById('mobile-user-email'); if (userEmailElement) { userEmailElement.textContent = currentUser.email; } if (mobileUserEmailElement) { mobileUserEmailElement.textContent = currentUser.email; } // Add admin link if user is admin if (currentUser.isAdmin) { // Add admin link to desktop header const headerActions = document.querySelector('.header-actions'); if (headerActions) { const adminLink = document.createElement('a'); adminLink.href = '/admin.html'; adminLink.className = 'btn btn-secondary'; adminLink.textContent = '⚙️ Admin'; headerActions.insertBefore(adminLink, headerActions.firstChild); } // Add admin link to mobile dropdown const mobileDropdownContent = document.getElementById('mobile-dropdown-content'); if (mobileDropdownContent) { // Check if admin link already exists if (!mobileDropdownContent.querySelector('.admin-link-mobile')) { const adminItem = document.createElement('div'); adminItem.className = 'mobile-dropdown-item admin-link-mobile'; const adminLink = document.createElement('a'); adminLink.href = '/admin.html'; adminLink.style.color = 'inherit'; adminLink.style.textDecoration = 'none'; adminLink.textContent = '⚙️ Admin Panel'; adminItem.appendChild(adminLink); // Insert admin link at the top of the dropdown if (mobileDropdownContent.firstChild) { mobileDropdownContent.insertBefore(adminItem, mobileDropdownContent.firstChild); } else { mobileDropdownContent.appendChild(adminItem); } } } } } // Initialize the map async function initializeMap() { try { // Get start location from server const response = await fetch('/api/admin/start-location'); const data = await response.json(); let startLat = CONFIG.DEFAULT_LAT; let startLng = CONFIG.DEFAULT_LNG; let startZoom = CONFIG.DEFAULT_ZOOM; if (data.success && data.location) { startLat = data.location.latitude; startLng = data.location.longitude; startZoom = data.location.zoom; } // Initialize map map = L.map('map').setView([startLat, startLng], startZoom); // Add tile layer 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 start location marker addStartLocationMarker(startLat, startLng); console.log('Map initialized successfully'); } catch (error) { console.error('Failed to initialize map:', error); showStatus('Failed to initialize map', 'error'); } } // Add start location marker function function addStartLocationMarker(lat, lng) { console.log(`Adding start location marker at: ${lat}, ${lng}`); // Remove existing start location marker if it exists if (startLocationMarker) { map.removeLayer(startLocationMarker); } // Create a very distinctive custom icon const startIcon = L.divIcon({ html: `
`, className: 'start-location-custom-marker', iconSize: [48, 48], iconAnchor: [24, 48], popupAnchor: [0, -48] }); // Create the marker startLocationMarker = L.marker([lat, lng], { icon: startIcon, zIndexOffset: 1000 }).addTo(map); // Add popup startLocationMarker.bindPopup(` `); } // Toggle start location visibility function toggleStartLocationVisibility() { if (!startLocationMarker) return; isStartLocationVisible = !isStartLocationVisible; if (isStartLocationVisible) { map.addLayer(startLocationMarker); // Update both desktop and mobile button text const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text'); if (desktopBtn) desktopBtn.textContent = 'Hide Start Location'; } else { map.removeLayer(startLocationMarker); // Update both desktop and mobile button text const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text'); if (desktopBtn) desktopBtn.textContent = 'Show Start Location'; } } // Load locations from the API async function loadLocations() { try { const response = await fetch('/api/locations'); const data = await response.json(); if (data.success) { displayLocations(data.locations); updateLocationCount(data.locations.length); } else { throw new Error(data.error || 'Failed to load locations'); } } catch (error) { console.error('Error loading locations:', error); showStatus('Failed to load locations', 'error'); } } // Display locations on the map function displayLocations(locations) { // Clear existing markers markers.forEach(marker => { if (marker && map) { map.removeLayer(marker); } }); markers = []; // Add new markers locations.forEach(location => { if (location.latitude && location.longitude) { const marker = createLocationMarker(location); if (marker) { // Only add if marker was successfully created markers.push(marker); } } }); console.log(`Displayed ${markers.length} locations`); } // Create a location marker function createLocationMarker(location) { // Safety check - ensure map exists if (!map) { console.warn('Map not initialized, skipping marker creation'); return null; } const lat = parseFloat(location.latitude); const lng = parseFloat(location.longitude); // Determine marker color based on support level let markerColor = 'blue'; if (location['Support Level']) { const level = parseInt(location['Support Level']); switch(level) { case 1: markerColor = 'green'; break; case 2: markerColor = 'yellow'; break; case 3: markerColor = 'orange'; break; case 4: markerColor = 'red'; break; } } const marker = L.circleMarker([lat, lng], { radius: 8, fillColor: markerColor, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.8 }).addTo(map); // Create popup content const popupContent = createPopupContent(location); marker.bindPopup(popupContent); // Store location data on marker for later use marker._locationData = location; return marker; } // Create popup content function createPopupContent(location) { // Try to find the ID field const locationId = location.Id || location.id || location.ID || location._id; const name = [location['First Name'], location['Last Name']] .filter(Boolean).join(' ') || 'Unknown'; const address = location.Address || 'No address'; const supportLevel = location['Support Level'] ? `Level ${location['Support Level']}` : 'Not specified'; return ` `; } // Setup event listeners function setupEventListeners() { // Desktop controls document.getElementById('refresh-btn')?.addEventListener('click', () => { loadLocations(); showStatus('Locations refreshed', 'success'); }); document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation); document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility); document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode); document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen); // Mobile controls document.getElementById('mobile-refresh-btn')?.addEventListener('click', () => { loadLocations(); showStatus('Locations refreshed', 'success'); }); document.getElementById('mobile-geolocate-btn')?.addEventListener('click', getUserLocation); document.getElementById('mobile-toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility); document.getElementById('mobile-add-location-btn')?.addEventListener('click', toggleAddLocationMode); document.getElementById('mobile-fullscreen-btn')?.addEventListener('click', toggleFullscreen); // Mobile dropdown toggle document.getElementById('mobile-dropdown-toggle')?.addEventListener('click', (e) => { e.stopPropagation(); const dropdown = document.getElementById('mobile-dropdown'); dropdown.classList.toggle('active'); }); // Close mobile dropdown when clicking outside document.addEventListener('click', (e) => { const dropdown = document.getElementById('mobile-dropdown'); if (!dropdown.contains(e.target)) { dropdown.classList.remove('active'); } }); // Modal controls document.getElementById('close-modal-btn')?.addEventListener('click', closeAddModal); document.getElementById('cancel-modal-btn')?.addEventListener('click', closeAddModal); // Edit footer controls document.getElementById('close-edit-footer-btn')?.addEventListener('click', closeEditForm); // Forms document.getElementById('location-form')?.addEventListener('submit', handleAddLocation); document.getElementById('edit-location-form')?.addEventListener('submit', handleEditLocation); // Delete button document.getElementById('delete-location-btn')?.addEventListener('click', handleDeleteLocation); // Address lookup buttons document.getElementById('lookup-address-add-btn')?.addEventListener('click', () => { lookupAddress('add'); }); document.getElementById('lookup-address-edit-btn')?.addEventListener('click', () => { lookupAddress('edit'); }); // Geo-location field sync setupGeoLocationSync(); // Add event delegation for popup edit buttons document.addEventListener('click', (e) => { if (e.target.classList.contains('edit-location-popup-btn')) { e.preventDefault(); try { const locationData = JSON.parse(e.target.getAttribute('data-location')); openEditForm(locationData); } catch (error) { console.error('Error parsing location data:', error); showStatus('Error opening edit form', 'error'); } } }); } // Setup geo-location field synchronization function setupGeoLocationSync() { // For add form const addLatInput = document.getElementById('location-lat'); const addLngInput = document.getElementById('location-lng'); const addGeoInput = document.getElementById('geo-location'); if (addLatInput && addLngInput && addGeoInput) { [addLatInput, addLngInput].forEach(input => { input.addEventListener('input', () => { const lat = addLatInput.value; const lng = addLngInput.value; if (lat && lng) { addGeoInput.value = `${lat};${lng}`; } }); }); addGeoInput.addEventListener('input', () => { const coords = parseGeoLocation(addGeoInput.value); if (coords) { addLatInput.value = coords.lat; addLngInput.value = coords.lng; } }); } // For edit form const editLatInput = document.getElementById('edit-location-lat'); const editLngInput = document.getElementById('edit-location-lng'); const editGeoInput = document.getElementById('edit-geo-location'); if (editLatInput && editLngInput && editGeoInput) { [editLatInput, editLngInput].forEach(input => { input.addEventListener('input', () => { const lat = editLatInput.value; const lng = editLngInput.value; if (lat && lng) { editGeoInput.value = `${lat};${lng}`; } }); }); editGeoInput.addEventListener('input', () => { const coords = parseGeoLocation(editGeoInput.value); if (coords) { editLatInput.value = coords.lat; editLngInput.value = coords.lng; } }); } } // Parse geo-location string function parseGeoLocation(value) { if (!value) return null; // Try semicolon separator first let parts = value.split(';'); if (parts.length !== 2) { // Try comma separator parts = value.split(','); } if (parts.length === 2) { const lat = parseFloat(parts[0].trim()); const lng = parseFloat(parts[1].trim()); if (!isNaN(lat) && !isNaN(lng)) { return { lat, lng }; } } return null; } // Get user location function getUserLocation() { if (!navigator.geolocation) { showStatus('Geolocation is not supported by your browser', 'error'); return; } showStatus('Getting your location...', 'info'); navigator.geolocation.getCurrentPosition( (position) => { const lat = position.coords.latitude; const lng = position.coords.longitude; // Center map on user location map.setView([lat, lng], 15); // Add or update user location marker if (userLocationMarker) { userLocationMarker.setLatLng([lat, lng]); } else { userLocationMarker = L.circleMarker([lat, lng], { radius: 10, fillColor: '#2196F3', color: '#fff', weight: 3, opacity: 1, fillOpacity: 0.8 }).addTo(map); userLocationMarker.bindPopup('Your Location'); } showStatus('Location found!', '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 toggleAddLocationMode() { isAddingLocation = !isAddingLocation; const crosshair = document.getElementById('crosshair'); const addBtn = document.getElementById('add-location-btn'); const mobileAddBtn = document.getElementById('mobile-add-location-btn'); if (isAddingLocation) { crosshair.classList.remove('hidden'); // Update desktop button if (addBtn) { addBtn.classList.add('active'); addBtn.innerHTML = 'Cancel'; } // Update mobile button if (mobileAddBtn) { mobileAddBtn.classList.add('active'); mobileAddBtn.innerHTML = '✕'; mobileAddBtn.title = 'Cancel'; } map.on('click', handleMapClick); } else { crosshair.classList.add('hidden'); // Update desktop button if (addBtn) { addBtn.classList.remove('active'); addBtn.innerHTML = 'Add Location Here'; } // Update mobile button if (mobileAddBtn) { mobileAddBtn.classList.remove('active'); mobileAddBtn.innerHTML = '➕'; mobileAddBtn.title = 'Add Location'; } map.off('click', handleMapClick); } } // Handle map click in add mode function handleMapClick(e) { if (!isAddingLocation) return; const { lat, lng } = e.latlng; openAddModal(lat, lng); toggleAddLocationMode(); } // Open add location modal function openAddModal(lat, lng) { const modal = document.getElementById('add-modal'); const latInput = document.getElementById('location-lat'); const lngInput = document.getElementById('location-lng'); const geoInput = document.getElementById('geo-location'); // Set coordinates latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; // Clear other fields document.getElementById('location-form').reset(); latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; // Show modal modal.classList.remove('hidden'); } // Close add modal function closeAddModal() { const modal = document.getElementById('add-modal'); modal.classList.add('hidden'); document.getElementById('location-form').reset(); } // Handle add location form submission async function handleAddLocation(e) { e.preventDefault(); const formData = new FormData(e.target); const data = {}; // Convert form data to object for (let [key, value] of formData.entries()) { // Map form field names to NocoDB column names if (key === 'latitude') data.latitude = value.trim(); else if (key === 'longitude') data.longitude = value.trim(); else if (key === 'Geo-Location') data['Geo-Location'] = value.trim(); else if (value.trim() !== '') { data[key] = value.trim(); } } // Ensure geo-location is set if (data.latitude && data.longitude) { data['Geo-Location'] = `${data.latitude};${data.longitude}`; } // Handle checkbox data.Sign = document.getElementById('sign').checked; try { const response = await fetch('/api/locations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { showStatus('Location added successfully!', 'success'); closeAddModal(); loadLocations(); } else { throw new Error(result.error || 'Failed to add location'); } } catch (error) { console.error('Error adding location:', error); showStatus(error.message || 'Failed to add location', 'error'); } } // Open edit form function openEditForm(location) { currentEditingLocation = location; // Debug: Log all possible ID fields console.log('Opening edit form for location:', { 'Id': location.Id, 'id': location.id, 'ID': location.ID, '_id': location._id, 'all_keys': Object.keys(location) }); // Extract ID - check multiple possible field names const locationId = location.Id || location.id || location.ID || location._id; if (!locationId) { console.error('No ID found in location object. Available fields:', Object.keys(location)); showStatus('Error: Location ID not found. Check console for details.', 'error'); return; } // Store the ID in a data attribute for later use document.getElementById('edit-location-id').value = locationId; document.getElementById('edit-location-id').setAttribute('data-location-id', locationId); // Populate form fields 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-phone').value = location.Phone || ''; document.getElementById('edit-location-unit').value = location['Unit Number'] || ''; document.getElementById('edit-support-level').value = location['Support Level'] || ''; document.getElementById('edit-location-address').value = location.Address || ''; 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-notes').value = location.Notes || ''; 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'] || ''; // Show edit footer document.getElementById('edit-footer').classList.remove('hidden'); } // Close edit form function closeEditForm() { document.getElementById('edit-footer').classList.add('hidden'); currentEditingLocation = null; } // Handle edit location form submission async function handleEditLocation(e) { e.preventDefault(); if (!currentEditingLocation) return; // Get the stored location ID const locationIdElement = document.getElementById('edit-location-id'); const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value; if (!locationId || locationId === 'undefined') { showStatus('Error: Location ID not found', 'error'); return; } const formData = new FormData(e.target); const data = {}; // Convert form data to object for (let [key, value] of formData.entries()) { // Skip the ID field if (key === 'id' || key === 'Id' || key === 'ID') continue; if (value !== null && value !== undefined) { // Don't skip empty strings - they may be intentional field clearing data[key] = value.trim(); } } // Ensure geo-location is set if (data.latitude && data.longitude) { data['Geo-Location'] = `${data.latitude};${data.longitude}`; } // Handle checkbox data.Sign = document.getElementById('edit-sign').checked; // Add debugging console.log('Sending update data for ID:', locationId); console.log('Update data:', data); try { const response = await fetch(`/api/locations/${locationId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const responseText = await response.text(); let result; try { result = JSON.parse(responseText); } catch (e) { console.error('Failed to parse response:', responseText); throw new Error(`Server response error: ${response.status} ${response.statusText}`); } if (result.success) { showStatus('Location updated successfully!', 'success'); closeEditForm(); loadLocations(); } else { throw new Error(result.error || 'Failed to update location'); } } catch (error) { console.error('Error updating location:', error); showStatus(`Update failed: ${error.message}`, 'error'); } } // Handle delete location async function handleDeleteLocation() { if (!currentEditingLocation) return; // Get the stored location ID const locationIdElement = document.getElementById('edit-location-id'); const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value; if (!locationId || locationId === 'undefined') { showStatus('Error: Location ID not found', 'error'); return; } 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 (result.success) { showStatus('Location deleted successfully!', 'success'); closeEditForm(); loadLocations(); } else { throw new Error(result.error || 'Failed to delete location'); } } catch (error) { console.error('Error deleting location:', error); showStatus(error.message || 'Failed to delete location', 'error'); } } // Lookup address based on current coordinates async function lookupAddress(mode) { let latInput, lngInput, addressInput; if (mode === 'add') { latInput = document.getElementById('location-lat'); lngInput = document.getElementById('location-lng'); addressInput = document.getElementById('location-address'); } else if (mode === 'edit') { latInput = document.getElementById('edit-location-lat'); lngInput = document.getElementById('edit-location-lng'); addressInput = document.getElementById('edit-location-address'); } else { console.error('Invalid lookup mode:', mode); return; } if (!latInput || !lngInput || !addressInput) { showStatus('Form elements not found', 'error'); return; } const lat = parseFloat(latInput.value); const lng = parseFloat(lngInput.value); if (isNaN(lat) || isNaN(lng)) { showStatus('Please enter valid coordinates first', 'warning'); return; } // Show loading state const button = mode === 'add' ? document.getElementById('lookup-address-add-btn') : document.getElementById('lookup-address-edit-btn'); const originalText = button ? button.textContent : ''; if (button) { button.disabled = true; button.textContent = 'Looking up...'; } try { console.log(`Looking up address for: ${lat}, ${lng}`); const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`); if (!response.ok) { const errorText = await response.text(); throw new Error(`Geocoding failed: ${response.status} ${errorText}`); } const data = await response.json(); if (data.success && data.data) { // Use the formatted address or full address const address = data.data.formattedAddress || data.data.fullAddress; if (address) { addressInput.value = address; showStatus('Address found!', 'success'); } else { showStatus('No address found for these coordinates', 'warning'); } } else { showStatus('Address lookup failed', 'warning'); } } catch (error) { console.error('Address lookup error:', error); showStatus(`Address lookup failed: ${error.message}`, 'error'); } finally { // Restore button state if (button) { button.disabled = false; button.textContent = originalText; } } } // Toggle fullscreen function toggleFullscreen() { const app = document.getElementById('app'); const btn = document.getElementById('fullscreen-btn'); const mobileBtn = document.getElementById('mobile-fullscreen-btn'); if (!document.fullscreenElement) { app.requestFullscreen().then(() => { app.classList.add('fullscreen'); // Update desktop button if (btn) { btn.innerHTML = 'Exit Fullscreen'; } // Update mobile button if (mobileBtn) { mobileBtn.innerHTML = '◱'; mobileBtn.title = 'Exit Fullscreen'; } }).catch(err => { console.error('Error entering fullscreen:', err); showStatus('Unable to enter fullscreen', 'error'); }); } else { document.exitFullscreen().then(() => { app.classList.remove('fullscreen'); // Update desktop button if (btn) { btn.innerHTML = 'Fullscreen'; } // Update mobile button if (mobileBtn) { mobileBtn.innerHTML = '⛶'; mobileBtn.title = 'Fullscreen'; } }); } } // Update location count function updateLocationCount(count) { const countElement = document.getElementById('location-count'); const mobileCountElement = document.getElementById('mobile-location-count'); const countText = `${count} location${count !== 1 ? 's' : ''}`; if (countElement) { countElement.textContent = countText; } if (mobileCountElement) { mobileCountElement.textContent = countText; } } // Setup auto-refresh function setupAutoRefresh() { refreshInterval = setInterval(() => { loadLocations(); }, CONFIG.REFRESH_INTERVAL); } // 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); } // Hide loading overlay function hideLoading() { const loading = document.getElementById('loading'); if (loading) { loading.classList.add('hidden'); } } // Escape HTML for security function escapeHtml(text) { if (text === null || text === undefined) { return ''; } const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Clean up on page unload window.addEventListener('beforeunload', () => { if (refreshInterval) { clearInterval(refreshInterval); } });