Udpates for the form system
This commit is contained in:
@@ -14,6 +14,7 @@ let markers = [];
|
||||
let userLocationMarker = null;
|
||||
let isAddingLocation = false;
|
||||
let refreshInterval = null;
|
||||
let currentEditingLocation = null; // Add this line
|
||||
|
||||
// Initialize application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -50,6 +51,9 @@ function initializeMap() {
|
||||
imperial: false
|
||||
}).addTo(map);
|
||||
|
||||
// Add legend
|
||||
addLegend();
|
||||
|
||||
// Hide loading overlay
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
}
|
||||
@@ -74,8 +78,170 @@ function setupEventListeners() {
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
|
||||
// Add legend to the map
|
||||
function addLegend() {
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
|
||||
legend.onAdd = function(map) {
|
||||
const div = L.DomUtil.create('div', 'map-legend');
|
||||
div.innerHTML = `
|
||||
<h4>Support Levels</h4>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color" style="background-color: #27ae60;"></span>
|
||||
<span>1 - Strong Support</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color" style="background-color: #f1c40f;"></span>
|
||||
<span>2 - Moderate Support</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color" style="background-color: #e67e22;"></span>
|
||||
<span>3 - Low Support</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color" style="background-color: #e74c3c;"></span>
|
||||
<span>4 - No Support</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color" style="background-color: #95a5a6;"></span>
|
||||
<span>Not Specified</span>
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
}
|
||||
|
||||
// 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 API configuration
|
||||
@@ -138,13 +304,35 @@ function displayLocations(locations) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create marker for location
|
||||
// Create marker for location (updated to use circle markers)
|
||||
function createLocationMarker(location) {
|
||||
const marker = L.marker([location.latitude, location.longitude], {
|
||||
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
|
||||
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);
|
||||
@@ -154,9 +342,11 @@ function createLocationMarker(location) {
|
||||
|
||||
// Create popup content for marker
|
||||
function createPopupContent(location) {
|
||||
console.log('Creating popup for location:', location);
|
||||
|
||||
let content = '<div class="popup-content">';
|
||||
|
||||
// Handle name from either 'title' field or 'First Name'/'Last Name' combination
|
||||
// Handle name
|
||||
let displayName = '';
|
||||
if (location.title) {
|
||||
displayName = location.title;
|
||||
@@ -170,31 +360,65 @@ function createPopupContent(location) {
|
||||
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>${escapeHtml(location.description)}</p>`;
|
||||
content += `<p><strong>Description:</strong> ${escapeHtml(location.description)}</p>`;
|
||||
}
|
||||
|
||||
if (location.category) {
|
||||
content += `<p><strong>Category:</strong> ${escapeHtml(location.category)}</p>`;
|
||||
}
|
||||
|
||||
if (location['Email']) {
|
||||
content += `<p><strong>Email:</strong> ${escapeHtml(location['Email'])}</p>`;
|
||||
}
|
||||
|
||||
if (location['Unit Number']) {
|
||||
content += `<p><strong>Unit:</strong> ${escapeHtml(location['Unit Number'])}</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
|
||||
content += `<div style="margin-top: 10px; text-align: center;">`;
|
||||
content += `<button class="btn btn-primary btn-sm" onclick="editLocation('${location.id || location.Id}')">✏️ Edit</button>`;
|
||||
content += '</div>';
|
||||
|
||||
content += '</div>';
|
||||
|
||||
return content;
|
||||
@@ -308,16 +532,26 @@ 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 = '';
|
||||
document.getElementById('location-address').value = '';
|
||||
document.getElementById('sign').checked = false;
|
||||
document.getElementById('sign-size').value = '';
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
@@ -333,6 +567,190 @@ 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 => m.options.locationData.id === locationId || m.options.locationData.Id === locationId)?.options.locationData;
|
||||
|
||||
if (!location) {
|
||||
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'] || '';
|
||||
document.getElementById('edit-location-address').value = location['Address'] || '';
|
||||
|
||||
// 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();
|
||||
@@ -347,6 +765,12 @@ async function handleLocationSubmit(e) {
|
||||
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',
|
||||
@@ -427,8 +851,11 @@ function showStatus(message, type = 'info') {
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
if (text === null || text === undefined) {
|
||||
return '';
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
div.textContent = String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
@@ -446,3 +873,6 @@ window.addEventListener('beforeunload', () => {
|
||||
|
||||
// Make closeModal function global for onclick handler
|
||||
window.closeModal = closeModal;
|
||||
window.editLocation = editLocation;
|
||||
window.closeEditFooter = closeEditFooter;
|
||||
window.deleteLocation = deleteLocation;
|
||||
|
||||
Reference in New Issue
Block a user