873 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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', () => {
console.log('DOM loaded, initializing application...');
checkAuth();
initializeMap();
loadLocations();
setupEventListeners();
setupAutoRefresh();
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';
return;
}
currentUser = data.user;
updateUserInterface();
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login.html';
}
}
// Update UI based on user
function updateUserInterface() {
if (!currentUser) return;
// Add user info and admin link to header if admin
const headerActions = document.querySelector('.header-actions');
if (currentUser.isAdmin && headerActions) {
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.className = 'btn btn-secondary';
adminLink.textContent = '⚙️ Admin';
headerActions.insertBefore(adminLink, headerActions.firstChild);
}
}
// 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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: `
<div class="start-location-marker-wrapper">
<div class="start-location-marker-pin">
<div class="start-location-marker-inner">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/>
</svg>
</div>
</div>
<div class="start-location-marker-pulse"></div>
</div>
`,
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(`
<div class="popup-content start-location-popup-enhanced">
<h3>📍 Map Start Location</h3>
<p>This is todays starting location!</p>
${currentUser?.isAdmin ? '<p><a href="/admin.html">Edit in Admin Panel</a></p>' : ''}
</div>
`);
}
// Toggle start location visibility
function toggleStartLocationVisibility() {
if (!startLocationMarker) return;
isStartLocationVisible = !isStartLocationVisible;
if (isStartLocationVisible) {
map.addLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').textContent = 'Hide Start Location';
} else {
map.removeLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').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 => map.removeLayer(marker));
markers = [];
// Add new markers
locations.forEach(location => {
if (location.latitude && location.longitude) {
const marker = createLocationMarker(location);
markers.push(marker);
}
});
console.log(`Displayed ${markers.length} locations`);
}
// Create a location marker
function createLocationMarker(location) {
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);
// Add click handler for editing - Store location data on marker
marker._locationData = location;
marker.on('click', () => {
if (currentUser) {
// Debug: Log the location object to see its structure
console.log('Location clicked:', location);
console.log('Available fields:', Object.keys(location));
setTimeout(() => openEditForm(location), 100);
}
});
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 `
<div class="popup-content">
<h3>${escapeHtml(name)}</h3>
<p><strong>Address:</strong> ${escapeHtml(address)}</p>
<p><strong>Support:</strong> ${escapeHtml(supportLevel)}</p>
${location.Sign ? '<p>🏁 Has campaign sign</p>' : ''}
${location.Notes ? `<p><strong>Notes:</strong> ${escapeHtml(location.Notes)}</p>` : ''}
<div class="popup-meta">
<p>ID: ${locationId || 'Unknown'}</p>
</div>
</div>
`;
}
// Setup event listeners
function setupEventListeners() {
// Refresh button
document.getElementById('refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
// Geolocate button
document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation);
// Toggle start location button
document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
// Add location button
document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode);
// Fullscreen button
document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// 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();
}
// 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('<strong>Your Location</strong>');
}
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');
if (isAddingLocation) {
crosshair.classList.remove('hidden');
addBtn.classList.add('active');
addBtn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
map.on('click', handleMapClick);
} else {
crosshair.classList.add('hidden');
addBtn.classList.remove('active');
addBtn.innerHTML = '<span class="btn-icon"></span><span class="btn-text">Add Location Here</span>';
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');
if (!document.fullscreenElement) {
app.requestFullscreen().then(() => {
app.classList.add('fullscreen');
btn.innerHTML = '<span class="btn-icon">◱</span><span class="btn-text">Exit Fullscreen</span>';
}).catch(err => {
console.error('Error entering fullscreen:', err);
showStatus('Unable to enter fullscreen', 'error');
});
} else {
document.exitFullscreen().then(() => {
app.classList.remove('fullscreen');
btn.innerHTML = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
});
}
}
// Update location count
function updateLocationCount(count) {
const countElement = document.getElementById('location-count');
if (countElement) {
countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`;
}
}
// 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);
}
});