Udpates for the form system

This commit is contained in:
admin 2025-06-22 13:26:27 -06:00
parent ac7dd74f30
commit ef176191fa
4 changed files with 769 additions and 37 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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();
@ -462,4 +505,5 @@ process.on('SIGTERM', () => {
logger.info('HTTP server closed');
process.exit(0);
});
});
});