1052 lines
36 KiB
JavaScript
1052 lines
36 KiB
JavaScript
// 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 = `
|
||
<span class="user-email">${escapeHtml(currentUser.email)}</span>
|
||
<button id="logout-btn" class="btn btn-secondary btn-sm" title="Sign out">
|
||
🚪 Logout
|
||
</button>
|
||
`;
|
||
|
||
// 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 = '<div class="popup-content">';
|
||
|
||
// Handle name
|
||
let displayName = '';
|
||
if (location.title) {
|
||
displayName = location.title;
|
||
} else if (location['First Name'] || location['Last Name']) {
|
||
const firstName = location['First Name'] || '';
|
||
const lastName = location['Last Name'] || '';
|
||
displayName = `${firstName} ${lastName}`.trim();
|
||
}
|
||
|
||
if (displayName) {
|
||
content += `<h3>${escapeHtml(displayName)}</h3>`;
|
||
}
|
||
|
||
// Support Level with color indicator
|
||
const supportColor = getSupportColor(location['Support Level']);
|
||
const supportText = getSupportLevelText(location['Support Level']);
|
||
content += `<p><strong>Support Level:</strong> <span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${supportColor}; margin-right: 5px;"></span>${escapeHtml(supportText)}</p>`;
|
||
|
||
// Display all available fields
|
||
if (location['Email']) {
|
||
content += `<p><strong>Email:</strong> ${escapeHtml(location['Email'])}</p>`;
|
||
}
|
||
|
||
if (location['Unit Number']) {
|
||
content += `<p><strong>Unit Number:</strong> ${escapeHtml(location['Unit Number'])}</p>`;
|
||
}
|
||
|
||
if (location['Address']) {
|
||
content += `<p><strong>Address:</strong> ${escapeHtml(location['Address'])}</p>`;
|
||
}
|
||
|
||
// Sign information
|
||
if (location['Sign']) {
|
||
content += `<p><strong>Has Sign:</strong> Yes`;
|
||
if (location['Sign Size']) {
|
||
content += ` (${escapeHtml(location['Sign Size'])})`;
|
||
}
|
||
content += '</p>';
|
||
}
|
||
|
||
if (location.description) {
|
||
content += `<p><strong>Description:</strong> ${escapeHtml(location.description)}</p>`;
|
||
}
|
||
|
||
if (location.category) {
|
||
content += `<p><strong>Category:</strong> ${escapeHtml(location.category)}</p>`;
|
||
}
|
||
|
||
content += '<div class="popup-meta">';
|
||
content += `<p><strong>Coordinates:</strong> ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}</p>`;
|
||
|
||
if (location['Geo-Location']) {
|
||
content += `<p><strong>Geo-Location:</strong> ${escapeHtml(location['Geo-Location'])}</p>`;
|
||
}
|
||
|
||
if (location.created_at) {
|
||
const date = new Date(location.created_at);
|
||
content += `<p><strong>Added:</strong> ${date.toLocaleDateString()}</p>`;
|
||
}
|
||
|
||
if (location.updated_at) {
|
||
const date = new Date(location.updated_at);
|
||
content += `<p><strong>Updated:</strong> ${date.toLocaleDateString()}</p>`;
|
||
}
|
||
|
||
content += '</div>';
|
||
|
||
// Add edit button with data attribute instead of onclick
|
||
content += `<div style="margin-top: 10px; text-align: center;">`;
|
||
content += `<button class="btn btn-primary btn-sm edit-location-btn" data-location-id="${location.id || location.Id}">✏️ Edit</button>`;
|
||
content += '</div>';
|
||
|
||
content += '</div>';
|
||
|
||
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: '<div style="background-color: #2c5aa0; width: 20px; height: 20px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);"></div>',
|
||
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 = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
|
||
btn.classList.remove('btn-success');
|
||
btn.classList.add('btn-secondary');
|
||
crosshair.classList.remove('hidden');
|
||
map.getContainer().style.cursor = 'crosshair';
|
||
} else {
|
||
btn.innerHTML = '<span class="btn-icon">➕</span><span class="btn-text">Add Location Here</span>';
|
||
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 = '<span class="btn-icon">✕</span><span class="btn-text">Exit Fullscreen</span>';
|
||
|
||
// 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 = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
|
||
|
||
// 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');
|
||
}
|
||
}
|