Udpates for the form system
This commit is contained in:
parent
ac7dd74f30
commit
ef176191fa
@ -142,6 +142,20 @@ body {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
|
||||
/* Crosshair for location selection */
|
||||
.crosshair {
|
||||
position: absolute;
|
||||
@ -326,6 +340,46 @@ body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Edit Footer Form */
|
||||
.edit-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border-top: 2px solid var(--primary-color);
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
||||
z-index: 1500;
|
||||
transition: transform 0.3s ease;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.edit-footer.hidden {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.edit-footer-content {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.edit-footer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.edit-footer-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
@ -362,11 +416,46 @@ body {
|
||||
box-shadow: 0 0 0 2px rgba(44, 90, 160, 0.1);
|
||||
}
|
||||
|
||||
.form-group input.valid {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.form-group input.invalid {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.form-group input[readonly] {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
transition: var(--transition);
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(44, 90, 160, 0.1);
|
||||
}
|
||||
|
||||
.form-group input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group label input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@ -445,6 +534,42 @@ body {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Map legend */
|
||||
.map-legend {
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.map-legend h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.legend-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
border: 1px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
|
||||
@ -55,6 +55,96 @@
|
||||
<!-- Status Messages -->
|
||||
<div id="status-container" class="status-container"></div>
|
||||
|
||||
<!-- Edit Location Footer Form -->
|
||||
<div id="edit-footer" class="edit-footer hidden">
|
||||
<div class="edit-footer-content">
|
||||
<div class="edit-footer-header">
|
||||
<h2>Edit Location</h2>
|
||||
<button class="btn btn-secondary btn-sm" onclick="closeEditFooter()">✕ Close</button>
|
||||
</div>
|
||||
<form id="edit-location-form">
|
||||
<input type="hidden" id="edit-location-id" name="id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="edit-first-name">First Name</label>
|
||||
<input type="text" id="edit-first-name" name="First Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-last-name">Last Name</label>
|
||||
<input type="text" id="edit-last-name" name="Last Name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-location-email">Email</label>
|
||||
<input type="email" id="edit-location-email" name="Email">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="edit-location-unit">Unit Number</label>
|
||||
<input type="text" id="edit-location-unit" name="Unit Number">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-support-level">Support Level</label>
|
||||
<select id="edit-support-level" name="Support Level">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="1">1 - Strong Support (Green)</option>
|
||||
<option value="2">2 - Moderate Support (Yellow)</option>
|
||||
<option value="3">3 - Low Support (Orange)</option>
|
||||
<option value="4">4 - No Support (Red)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-location-address">Address</label>
|
||||
<input type="text" id="edit-location-address" name="Address">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="edit-sign" name="Sign" value="true">
|
||||
Has Campaign Sign
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-sign-size">Sign Size</label>
|
||||
<select id="edit-sign-size" name="Sign Size">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="Small">Small</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Large">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="edit-location-lat">Latitude</label>
|
||||
<input type="number" id="edit-location-lat" name="latitude" step="0.00000001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-location-lng">Longitude</label>
|
||||
<input type="number" id="edit-location-lng" name="longitude" step="0.00000001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-geo-location">Geo-Location</label>
|
||||
<input type="text" id="edit-geo-location" name="Geo-Location"
|
||||
placeholder="e.g., 53.5461;-113.4938">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-danger" onclick="deleteLocation()">Delete</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Location Modal -->
|
||||
<div id="add-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
@ -83,10 +173,46 @@
|
||||
placeholder="Enter email address">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="location-unit">Unit Number</label>
|
||||
<input type="text" id="location-unit" name="Unit Number"
|
||||
placeholder="Enter unit number">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="support-level">Support Level</label>
|
||||
<select id="support-level" name="Support Level">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="1">1 - Strong Support (Green)</option>
|
||||
<option value="2">2 - Moderate Support (Yellow)</option>
|
||||
<option value="3">3 - Low Support (Orange)</option>
|
||||
<option value="4">4 - No Support (Red)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location-unit">Unit Number</label>
|
||||
<input type="text" id="location-unit" name="Unit Number"
|
||||
placeholder="Enter unit number">
|
||||
<label for="location-address">Address</label>
|
||||
<input type="text" id="location-address" name="Address"
|
||||
placeholder="Enter address">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="sign" name="Sign" value="true">
|
||||
Has Campaign Sign
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sign-size">Sign Size</label>
|
||||
<select id="sign-size" name="Sign Size">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="Small">Small</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Large">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
@ -102,6 +228,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="geo-location">Geo-Location</label>
|
||||
<input type="text" id="geo-location" name="Geo-Location"
|
||||
placeholder="e.g., 53.5461;-113.4938"
|
||||
title="Enter as 'latitude;longitude' or 'latitude, longitude'">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">
|
||||
Cancel
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -31,6 +31,53 @@ function parseNocoDBUrl(url) {
|
||||
return { projectId: null, tableId: null };
|
||||
}
|
||||
|
||||
// Add this helper function near the top of the file after the parseNocoDBUrl function
|
||||
function syncGeoFields(data) {
|
||||
// If we have latitude and longitude but no Geo-Location, create it
|
||||
if (data.latitude && data.longitude && !data['Geo-Location']) {
|
||||
const lat = parseFloat(data.latitude);
|
||||
const lng = parseFloat(data.longitude);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
data['Geo-Location'] = `${lat};${lng}`; // Use semicolon format for NocoDB GeoData
|
||||
data.geodata = `${lat};${lng}`; // Also update geodata for compatibility
|
||||
}
|
||||
}
|
||||
|
||||
// If we have Geo-Location but no lat/lng, parse it
|
||||
else if (data['Geo-Location'] && (!data.latitude || !data.longitude)) {
|
||||
const geoLocation = data['Geo-Location'].toString();
|
||||
|
||||
// Try semicolon-separated first
|
||||
let parts = geoLocation.split(';');
|
||||
if (parts.length === 2) {
|
||||
const lat = parseFloat(parts[0].trim());
|
||||
const lng = parseFloat(parts[1].trim());
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
data.latitude = lat;
|
||||
data.longitude = lng;
|
||||
data.geodata = `${lat};${lng}`;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// Try comma-separated
|
||||
parts = geoLocation.split(',');
|
||||
if (parts.length === 2) {
|
||||
const lat = parseFloat(parts[0].trim());
|
||||
const lng = parseFloat(parts[1].trim());
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
data.latitude = lat;
|
||||
data.longitude = lng;
|
||||
data.geodata = `${lat};${lng}`;
|
||||
// Normalize Geo-Location to semicolon format for NocoDB GeoData
|
||||
data['Geo-Location'] = `${lat};${lng}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Auto-parse IDs if view URL is provided
|
||||
if (process.env.NOCODB_VIEW_URL && (!process.env.NOCODB_PROJECT_ID || !process.env.NOCODB_TABLE_ID)) {
|
||||
const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_VIEW_URL);
|
||||
@ -154,6 +201,9 @@ app.get('/api/locations', async (req, res) => {
|
||||
const locations = response.data.list || [];
|
||||
|
||||
const validLocations = locations.filter(loc => {
|
||||
// Apply geo field synchronization to each location
|
||||
loc = syncGeoFields(loc);
|
||||
|
||||
// Check if location has valid coordinates
|
||||
if (loc.latitude && loc.longitude) {
|
||||
return true;
|
||||
@ -256,7 +306,12 @@ app.get('/api/locations/:id', async (req, res) => {
|
||||
// Create new location
|
||||
app.post('/api/locations', strictLimiter, async (req, res) => {
|
||||
try {
|
||||
const { latitude, longitude, ...additionalData } = req.body;
|
||||
let locationData = { ...req.body };
|
||||
|
||||
// Sync geo fields before validation
|
||||
locationData = syncGeoFields(locationData);
|
||||
|
||||
const { latitude, longitude, ...additionalData } = locationData;
|
||||
|
||||
// Validate coordinates
|
||||
if (!latitude || !longitude) {
|
||||
@ -310,10 +365,10 @@ app.post('/api/locations', strictLimiter, async (req, res) => {
|
||||
|
||||
// Format geodata in both formats for compatibility
|
||||
const geodata = `${lat};${lng}`;
|
||||
const geoLocation = `${lat}, ${lng}`;
|
||||
const geoLocation = `${lat};${lng}`; // Use semicolon format for NocoDB GeoData column
|
||||
|
||||
// Prepare data for NocoDB
|
||||
const locationData = {
|
||||
const finalData = {
|
||||
geodata,
|
||||
'Geo-Location': geoLocation,
|
||||
latitude: lat,
|
||||
@ -326,7 +381,7 @@ app.post('/api/locations', strictLimiter, async (req, res) => {
|
||||
|
||||
logger.info('Creating new location:', { lat, lng });
|
||||
|
||||
const response = await axios.post(url, locationData, {
|
||||
const response = await axios.post(url, finalData, {
|
||||
headers: {
|
||||
'xc-token': process.env.NOCODB_API_TOKEN,
|
||||
'Content-Type': 'application/json'
|
||||
@ -361,22 +416,10 @@ app.post('/api/locations', strictLimiter, async (req, res) => {
|
||||
// Update location
|
||||
app.put('/api/locations/:id', strictLimiter, async (req, res) => {
|
||||
try {
|
||||
const { latitude, longitude, ...additionalData } = req.body;
|
||||
let updateData = { ...req.body };
|
||||
|
||||
const updateData = { ...additionalData };
|
||||
|
||||
// Update geodata if coordinates changed
|
||||
if (latitude !== undefined && longitude !== undefined) {
|
||||
const lat = parseFloat(latitude);
|
||||
const lng = parseFloat(longitude);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
updateData.latitude = lat;
|
||||
updateData.longitude = lng;
|
||||
updateData.geodata = `${lat};${lng}`;
|
||||
updateData['Geo-Location'] = `${lat}, ${lng}`;
|
||||
}
|
||||
}
|
||||
// Sync geo fields
|
||||
updateData = syncGeoFields(updateData);
|
||||
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
|
||||
@ -463,3 +506,4 @@ process.on('SIGTERM', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user