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 = `
+
+ `;
+
+ 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