From ef176191fa886ca613ef530043497967f3fe084e Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 22 Jun 2025 13:26:27 -0600 Subject: [PATCH] Udpates for the form system --- nocodb-map-viewer/app/public/css/style.css | 125 ++++++ nocodb-map-viewer/app/public/index.html | 139 ++++++- nocodb-map-viewer/app/public/js/map.js | 458 ++++++++++++++++++++- nocodb-map-viewer/app/server.js | 84 +++- 4 files changed, 769 insertions(+), 37 deletions(-) diff --git a/nocodb-map-viewer/app/public/css/style.css b/nocodb-map-viewer/app/public/css/style.css index ce80a3d..50dca5c 100644 --- a/nocodb-map-viewer/app/public/css/style.css +++ b/nocodb-map-viewer/app/public/css/style.css @@ -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 { diff --git a/nocodb-map-viewer/app/public/index.html b/nocodb-map-viewer/app/public/index.html index d0ee844..de9a316 100644 --- a/nocodb-map-viewer/app/public/index.html +++ b/nocodb-map-viewer/app/public/index.html @@ -55,6 +55,96 @@
+ + + +
+ + +
+
`; + content += '
'; + content += ''; 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; diff --git a/nocodb-map-viewer/app/server.js b/nocodb-map-viewer/app/server.js index 24679b6..ce2f952 100644 --- a/nocodb-map-viewer/app/server.js +++ b/nocodb-map-viewer/app/server.js @@ -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); }); -}); \ No newline at end of file +}); +