diff --git a/map/app/public/css/style.css b/map/app/public/css/style.css index 0ff22ea..15416e7 100644 --- a/map/app/public/css/style.css +++ b/map/app/public/css/style.css @@ -111,6 +111,61 @@ body { z-index: 1000; } +/* Move Controls */ +.move-controls { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10000; + min-width: 300px; + text-align: center; +} + +/* Mobile-specific move controls */ +@media (max-width: 768px) { + .move-controls { + top: auto; + bottom: 20px; + left: 10px; + right: 10px; + transform: none; + width: auto; + min-width: unset; + max-width: 100%; + padding: 15px; + z-index: 10001; /* Ensure it's on top */ + background: white; /* Add background */ + box-shadow: 0 -2px 10px rgba(0,0,0,0.1); /* Add shadow for visibility */ + } + + .move-controls-content h3 { + font-size: 18px; + margin-bottom: 8px; + } + + .move-controls-content p { + font-size: 14px; + margin: 3px 0; + } + + .move-controls-actions { + flex-direction: column; + gap: 8px; + margin-top: 12px; + } + + .move-controls-actions .btn { + width: 100%; + padding: 12px 16px; + font-size: 16px; + } +} + /* Buttons */ .btn { padding: 10px 16px; @@ -986,3 +1041,96 @@ path.leaflet-interactive { stroke-width: 2px !important; fill-opacity: 0.8 !important; } + +/* Marker being moved */ +.location-marker.leaflet-drag-target { + cursor: move !important; +} + +/* Popup actions buttons spacing */ +.popup-actions { + display: flex; + gap: 5px; + margin-top: 10px; +} + +/* Pulsing animation for marker being moved */ +@keyframes pulse-marker { + 0% { + transform: scale(1); + opacity: 0.9; + } + 50% { + transform: scale(1.2); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.9; + } +} + +/* Ensure marker animations work */ +.leaflet-overlay-pane svg path { + transform-origin: center; +} + +/* Move confirmation popup styles */ +.move-confirm-popup-wrapper .leaflet-popup-content-wrapper { + background: white; + border: 2px solid var(--primary-color); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +.move-confirm-popup { + text-align: center; + padding: 10px; +} + +.move-confirm-popup h3 { + margin: 0 0 10px 0; + color: var(--dark-color); + font-size: 16px; +} + +.move-confirm-popup p { + margin: 0 0 15px 0; + color: #666; +} + +.move-confirm-popup .popup-actions { + display: flex; + gap: 10px; + justify-content: center; +} + +/* Cancel move button styles */ +#cancel-move-btn, +#mobile-cancel-move-btn { + background-color: var(--danger-color); + color: white; +} + +#cancel-move-btn:hover, +#mobile-cancel-move-btn:hover { + background-color: #c0392b; +} + +/* Ensure crosshairs are visible during move */ +.crosshair { + z-index: 1100; +} + +/* Mobile-friendly popup buttons */ +@media (max-width: 768px) { + .popup-content .popup-actions .btn { + padding: 10px 12px; + font-size: 14px; + min-height: 44px; /* Ensure touch-friendly size */ + } + + .move-confirm-popup .popup-actions .btn { + min-width: 100px; + min-height: 44px; + } +} diff --git a/map/app/public/js/location-manager.js b/map/app/public/js/location-manager.js index 91218c9..30cf1bc 100644 --- a/map/app/public/js/location-manager.js +++ b/map/app/public/js/location-manager.js @@ -7,6 +7,12 @@ import { resetAddressConfirmation } from './ui-controls.js'; export let markers = []; export let currentEditingLocation = null; +// Add these variables at the top with other exports +export let isMovingMarker = false; +export let movingMarker = null; +export let originalPosition = null; +export let movingLocationData = null; + export async function loadLocations() { try { const response = await fetch('/api/locations'); @@ -96,12 +102,69 @@ function createLocationMarker(location) { weight: 2, opacity: 1, fillOpacity: 0.8, - className: 'location-marker' // Add a class for CSS targeting + className: 'location-marker' }); // Add to map marker.addTo(map); + // Add custom dragging functionality for circle markers + let _draggingEnabled = false; + let isDragging = false; + let dragStartLatLng = null; + + marker.dragging = { + enabled: function() { + return _draggingEnabled; + }, + enable: function() { + _draggingEnabled = true; + marker.on('mousedown', startDrag); + marker._path.style.cursor = 'move'; + }, + disable: function() { + _draggingEnabled = false; + marker.off('mousedown', startDrag); + marker.off('mousemove', drag); + marker.off('mouseup', endDrag); + map.off('mousemove', drag); + map.off('mouseup', endDrag); + marker._path.style.cursor = 'pointer'; + } + }; + + function startDrag(e) { + if (!_draggingEnabled) return; + isDragging = true; + dragStartLatLng = e.latlng; + map.dragging.disable(); + marker.on('mousemove', drag); + map.on('mousemove', drag); + marker.on('mouseup', endDrag); + map.on('mouseup', endDrag); + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + } + + function drag(e) { + if (!isDragging) return; + marker.setLatLng(e.latlng); + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + } + + function endDrag(e) { + if (!isDragging) return; + isDragging = false; + map.dragging.enable(); + marker.off('mousemove', drag); + map.off('mousemove', drag); + marker.off('mouseup', endDrag); + map.off('mouseup', endDrag); + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + } + const popupContent = createPopupContent(location); marker.bindPopup(popupContent); marker._locationData = location; @@ -120,6 +183,9 @@ function createPopupContent(location) { const supportLevel = location['Support Level'] ? `Level ${location['Support Level']}` : 'Not specified'; + // Add debugging + console.log('Creating popup for location:', locationId, location); + return ` ` : ''} @@ -388,3 +458,198 @@ export function openAddModal(lat, lng) { }); document.dispatchEvent(autoLookupEvent); } + +// Replace the startMovingMarker function +export function startMovingMarker(location, marker) { + console.log('startMovingMarker called with:', location, marker); + + if (!location) { + console.error('Missing location data'); + return; + } + + const locationId = location.Id || location.id || location.ID || location._id; + if (!locationId) { + showStatus('Error: Location ID not found', 'error'); + return; + } + + // Store the location data and original position + movingLocationData = location; + originalPosition = marker ? marker.getLatLng() : null; + isMovingMarker = true; + + // Close any popups + map.closePopup(); + + // Show crosshairs + const crosshair = document.getElementById('crosshair'); + const crosshairInfo = crosshair.querySelector('.crosshair-info'); + crosshairInfo.textContent = 'Click to move location here'; + crosshair.classList.remove('hidden'); + + // Update buttons to show cancel state - use event system + document.dispatchEvent(new CustomEvent('updateMoveButtons', { detail: { isMoving: true } })); + + // Show instructions + const name = [location['First Name'], location['Last Name']] + .filter(Boolean).join(' ') || 'Location'; + showStatus(`Moving "${name}". Click anywhere on the map to set new position.`, 'info'); + + // Add click handler to map + map.on('click', handleMoveMapClick); + + // Highlight the marker being moved + if (marker) { + movingMarker = marker; + marker.setStyle({ + fillColor: '#ff7800', + fillOpacity: 0.9, + radius: 10 + }); + } +} + +// Add new function to handle map clicks during move +function handleMoveMapClick(e) { + if (!isMovingMarker || !movingLocationData) return; + + const { lat, lng } = e.latlng; + + // Show confirmation dialog + showMoveConfirmation(lat, lng); +} + +// Add function to show move confirmation +function showMoveConfirmation(lat, lng) { + const name = [movingLocationData['First Name'], movingLocationData['Last Name']] + .filter(Boolean).join(' ') || 'Location'; + + // Create a temporary marker at the new position + const tempMarker = L.circleMarker([lat, lng], { + radius: 10, + fillColor: '#ff7800', + color: '#fff', + weight: 3, + opacity: 1, + fillOpacity: 0.5 + }).addTo(map); + + // Create confirmation popup + const confirmContent = ` +
+

Confirm Move

+

Move "${escapeHtml(name)}" to this location?

+ +
+ `; + + tempMarker.bindPopup(confirmContent, { + closeButton: false, + className: 'move-confirm-popup-wrapper' + }).openPopup(); + + // Add event listeners + setTimeout(() => { + document.getElementById('confirm-move-btn')?.addEventListener('click', async () => { + map.removeLayer(tempMarker); + await saveMovePosition(lat, lng); + }); + + document.getElementById('cancel-move-btn')?.addEventListener('click', () => { + map.removeLayer(tempMarker); + // Continue move mode + }); + }, 100); +} + +// Update saveMovePosition to accept coordinates +async function saveMovePosition(lat, lng) { + if (!movingLocationData) return; + + const locationId = movingLocationData.Id || movingLocationData.id || + movingLocationData.ID || movingLocationData._id; + + if (!locationId) { + showStatus('Error: Location ID not found', 'error'); + cancelMove(); + return; + } + + // Prepare update data + const updateData = { + latitude: lat.toFixed(8), + longitude: lng.toFixed(8), + 'Geo-Location': `${lat.toFixed(8)};${lng.toFixed(8)}` + }; + + try { + const response = await fetch(`/api/locations/${locationId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updateData) + }); + + const result = await response.json(); + + if (result.success) { + showStatus('Location moved successfully!', 'success'); + cleanupMoveState(); // Use cleanup instead of cancelMove + + // Reload locations to update the map + await loadLocations(); + } else { + throw new Error(result.error || 'Failed to update location'); + } + } catch (error) { + console.error('Error moving location:', error); + showStatus(`Failed to move location: ${error.message}`, 'error'); + cancelMove(); + } +} + +// Add new function to clean up move state without showing cancellation message +function cleanupMoveState() { + // Hide crosshairs + const crosshair = document.getElementById('crosshair'); + crosshair.classList.add('hidden'); + + // Remove map click handler + map.off('click', handleMoveMapClick); + + // Reset buttons - use event system + document.dispatchEvent(new CustomEvent('updateMoveButtons', { detail: { isMoving: false } })); + + // Reset marker style if exists + if (movingMarker && originalPosition) { + // Restore original marker style based on support level + const location = movingMarker._locationData; + let markerColor = '#3388ff'; + if (location && location['Support Level']) { + const level = parseInt(location['Support Level']); + switch(level) { + case 1: markerColor = '#27ae60'; break; + case 2: markerColor = '#f1c40f'; break; + case 3: markerColor = '#e67e22'; break; + case 4: markerColor = '#e74c3c'; break; + } + } + + movingMarker.setStyle({ + fillColor: markerColor, + fillOpacity: 0.8, + radius: 8 + }); + } + + // Reset state + isMovingMarker = false; + movingMarker = null; + originalPosition = null; + movingLocationData = null; +} \ No newline at end of file diff --git a/map/app/public/js/ui-controls.js b/map/app/public/js/ui-controls.js index 3faf1aa..0d8d7c0 100644 --- a/map/app/public/js/ui-controls.js +++ b/map/app/public/js/ui-controls.js @@ -8,6 +8,9 @@ export let isAddingLocation = false; export let isAddressConfirmed = false; export let isEditAddressConfirmed = false; +// Add this after the existing imports +let isMoveMode = false; + // Export function to get current confirmation states export function getAddressConfirmationState() { return { @@ -509,5 +512,178 @@ export function setupEventListeners() { showStatus('Error opening edit form', 'error'); } } + + // Add handler for move button + if (e.target.classList.contains('move-location-popup-btn')) { + e.preventDefault(); + try { + const locationData = JSON.parse(e.target.getAttribute('data-location')); + // Find the marker that corresponds to this location + import('./location-manager.js').then(({ markers, startMovingMarker }) => { + const marker = markers.find(m => { + const markerData = m._locationData; + const locationId = locationData.Id || locationData.id || locationData.ID || locationData._id; + const markerId = markerData?.Id || markerData?.id || markerData?.ID || markerData?._id; + return markerId === locationId; + }); + + if (marker) { + startMovingMarker(locationData, marker); + } else { + console.error('Could not find marker for location:', locationData); + showStatus('Could not find marker for this location', 'error'); + } + }); + } catch (error) { + console.error('Error starting move:', error); + showStatus('Failed to start move mode', 'error'); + } + } }); + + // Improve popup button handling for mobile + map.on('popupopen', function(e) { + const popup = e.popup; + const content = popup.getContent(); + + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; + + // Find buttons in the popup + const editBtn = tempDiv.querySelector('.edit-location-popup-btn'); + const moveBtn = tempDiv.querySelector('.move-location-popup-btn'); + + if (editBtn || moveBtn) { + // Re-render popup with proper event handlers + setTimeout(() => { + const popupNode = popup._contentNode || popup._container; + if (!popupNode) return; + + // Edit button + const editButton = popupNode.querySelector('.edit-location-popup-btn'); + if (editButton) { + editButton.addEventListener('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + try { + const locationData = JSON.parse(this.getAttribute('data-location')); + openEditForm(locationData); + map.closePopup(); + } catch (error) { + console.error('Error parsing location data:', error); + showStatus('Error opening edit form', 'error'); + } + }); + } + + // Move button + const moveButton = popupNode.querySelector('.move-location-popup-btn'); + if (moveButton) { + moveButton.addEventListener('click', async function(event) { + event.preventDefault(); + event.stopPropagation(); + try { + const locationData = JSON.parse(this.getAttribute('data-location')); + const { startMovingMarker } = await import('./location-manager.js'); + startMovingMarker(locationData, e.popup._source); + } catch (error) { + console.error('Error starting move:', error); + showStatus('Failed to start move mode', 'error'); + } + }); + } + }, 0); + } + }); + + // Add this after the existing event listeners + document.addEventListener('updateMoveButtons', (e) => { + isMoveMode = e.detail.isMoving; + + if (isMoveMode) { + // Hide add location buttons during move mode + const addBtn = document.getElementById('add-location-btn'); + const mobileAddBtn = document.getElementById('mobile-add-location-btn'); + + if (addBtn) addBtn.style.display = 'none'; + if (mobileAddBtn) mobileAddBtn.style.display = 'none'; + + // Add cancel move button + addCancelMoveButton(); + } else { + // Restore add location buttons + const addBtn = document.getElementById('add-location-btn'); + const mobileAddBtn = document.getElementById('mobile-add-location-btn'); + + if (addBtn) addBtn.style.display = ''; + if (mobileAddBtn) mobileAddBtn.style.display = ''; + + // Remove cancel move button + removeCancelMoveButton(); + } + }); +} + +// Add this to the setupMapEventListeners function +export function setupMapEventListeners(mapInstance) { + // ...existing code... + + // Handle move location button clicks in popups + mapInstance.on('popupopen', function(e) { + // ...existing code for edit button... + + // Handle move button + const moveBtn = e.popup._contentNode?.querySelector('.move-location-popup-btn'); + if (moveBtn) { + moveBtn.addEventListener('click', async function() { + try { + const locationData = JSON.parse(this.getAttribute('data-location')); + const marker = e.popup._source; + + const { startMovingMarker } = await import('./location-manager.js'); + startMovingMarker(locationData, marker); + } catch (error) { + console.error('Error starting move:', error); + showStatus('Failed to start move mode', 'error'); + } + }); + } + }); +} + +function addCancelMoveButton() { + // Desktop button + const mapControls = document.querySelector('.map-controls'); + if (mapControls && !document.getElementById('cancel-move-btn')) { + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'cancel-move-btn'; + cancelBtn.className = 'btn btn-danger'; + cancelBtn.innerHTML = 'Cancel Move'; + cancelBtn.addEventListener('click', async () => { + const { cancelMove } = await import('./location-manager.js'); + cancelMove(); + }); + mapControls.insertBefore(cancelBtn, mapControls.firstChild); + } + + // Mobile button + const mobileSidebar = document.getElementById('mobile-sidebar'); + if (mobileSidebar && !document.getElementById('mobile-cancel-move-btn')) { + const mobileCancelBtn = document.createElement('button'); + mobileCancelBtn.id = 'mobile-cancel-move-btn'; + mobileCancelBtn.className = 'btn btn-danger'; + mobileCancelBtn.title = 'Cancel Move'; + mobileCancelBtn.innerHTML = '✕'; + mobileCancelBtn.addEventListener('click', async () => { + const { cancelMove } = await import('./location-manager.js'); + cancelMove(); + }); + mobileSidebar.insertBefore(mobileCancelBtn, mobileSidebar.firstChild); + } +} + +function removeCancelMoveButton() { + document.getElementById('cancel-move-btn')?.remove(); + document.getElementById('mobile-cancel-move-btn')?.remove(); } \ No newline at end of file