1132 lines
41 KiB
JavaScript
1132 lines
41 KiB
JavaScript
// Helper function to escape HTML to prevent XSS
|
|
function escapeHtml(text) {
|
|
if (typeof text !== 'string') return text;
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
}
|
|
|
|
// Location management (CRUD operations)
|
|
import { map } from './map-manager.js';
|
|
import { showStatus, updateLocationCount } from './utils.js';
|
|
import { currentUser } from './auth.js';
|
|
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;
|
|
let originalIcon = null;
|
|
|
|
export async function loadLocations() {
|
|
try {
|
|
showStatus('Loading locations...', 'info');
|
|
|
|
const response = await fetch('/api/locations');
|
|
|
|
// Handle rate limit responses
|
|
if (response.status === 429) {
|
|
let errorMessage = 'Too many requests. Please wait a moment.';
|
|
try {
|
|
const errorData = await response.json();
|
|
if (errorData.error) {
|
|
errorMessage = errorData.error;
|
|
}
|
|
} catch (e) {
|
|
// If we can't parse the JSON, use the default message
|
|
}
|
|
console.warn('Rate limited:', errorMessage);
|
|
showStatus(errorMessage, 'warning');
|
|
return; // Don't throw an error, just return
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Failed to load locations');
|
|
}
|
|
|
|
// Check if data is limited (temp user)
|
|
if (data.isLimited && currentUser?.userType === 'temp') {
|
|
console.log('Map data loaded'); // Generic message for temp users
|
|
} else {
|
|
console.log(`Loaded ${data.count} locations from NocoDB`);
|
|
}
|
|
|
|
displayLocations(data.locations);
|
|
updateLocationCount(data.locations.length);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading locations:', error);
|
|
showStatus('Failed to load locations', 'error');
|
|
}
|
|
}
|
|
|
|
export function displayLocations(locations) {
|
|
// Clear existing markers
|
|
markers.forEach(marker => {
|
|
if (marker && map) {
|
|
map.removeLayer(marker);
|
|
}
|
|
});
|
|
markers = [];
|
|
|
|
// Group locations by address to identify multi-unit buildings
|
|
const addressGroups = new Map();
|
|
|
|
locations.forEach(location => {
|
|
if (location.latitude && location.longitude) {
|
|
const address = location.Address || 'No address';
|
|
const addressKey = address.toLowerCase().trim();
|
|
|
|
if (!addressGroups.has(addressKey)) {
|
|
addressGroups.set(addressKey, {
|
|
address: address,
|
|
locations: [],
|
|
lat: parseFloat(location.latitude),
|
|
lng: parseFloat(location.longitude)
|
|
});
|
|
}
|
|
|
|
addressGroups.get(addressKey).locations.push(location);
|
|
}
|
|
});
|
|
|
|
// Create markers for each address group
|
|
addressGroups.forEach(group => {
|
|
if (group.locations.length > 1) {
|
|
// Multi-unit building - create apartment-style marker
|
|
const marker = createMultiUnitMarker(group);
|
|
if (marker) {
|
|
markers.push(marker);
|
|
}
|
|
} else {
|
|
// Single unit - create regular marker
|
|
const marker = createLocationMarker(group.locations[0]);
|
|
if (marker) {
|
|
markers.push(marker);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Limit console output for temp users
|
|
if (currentUser?.userType === 'temp') {
|
|
console.log('Map built successfully');
|
|
} else {
|
|
console.log(`Displayed ${markers.length} location markers (${locations.length} total locations)`);
|
|
}
|
|
}
|
|
|
|
function createLocationMarker(location) {
|
|
if (!map) {
|
|
console.warn('Map not initialized, skipping marker creation');
|
|
return null;
|
|
}
|
|
|
|
// Try to get coordinates from multiple possible sources
|
|
let lat, lng;
|
|
|
|
// First try the Geo-Location field
|
|
if (location['Geo-Location']) {
|
|
const coords = location['Geo-Location'].split(';');
|
|
if (coords.length === 2) {
|
|
lat = parseFloat(coords[0]);
|
|
lng = parseFloat(coords[1]);
|
|
}
|
|
}
|
|
|
|
// If that didn't work, try latitude/longitude fields
|
|
if ((!lat || !lng) && location.latitude && location.longitude) {
|
|
lat = parseFloat(location.latitude);
|
|
lng = parseFloat(location.longitude);
|
|
}
|
|
|
|
// Validate coordinates
|
|
if (!lat || !lng || isNaN(lat) || isNaN(lng)) {
|
|
console.warn('Invalid coordinates for location:', location);
|
|
return null;
|
|
}
|
|
|
|
// Determine marker color based on support level
|
|
let markerColor = '#3388ff'; // Default blue
|
|
if (location['Support Level']) {
|
|
const level = parseInt(location['Support Level']);
|
|
switch(level) {
|
|
case 1: markerColor = '#27ae60'; break; // Green
|
|
case 2: markerColor = '#f1c40f'; break; // Yellow
|
|
case 3: markerColor = '#e67e22'; break; // Orange
|
|
case 4: markerColor = '#e74c3c'; break; // Red
|
|
}
|
|
}
|
|
|
|
// Create circle marker with explicit styling
|
|
const marker = L.circleMarker([lat, lng], {
|
|
radius: 8,
|
|
fillColor: markerColor,
|
|
color: '#ffffff',
|
|
weight: 2,
|
|
opacity: 1,
|
|
fillOpacity: 0.8,
|
|
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;
|
|
|
|
// Only log marker creation for non-temp users
|
|
if (currentUser?.userType !== 'temp') {
|
|
console.log(`Created marker at ${lat}, ${lng} with color ${markerColor}`);
|
|
}
|
|
|
|
return marker;
|
|
}
|
|
|
|
function createPopupContent(location) {
|
|
const locationId = location.Id || location.id || location.ID || location._id;
|
|
|
|
// If current user is temp, show limited information but allow editing
|
|
if (currentUser?.userType === 'temp') {
|
|
const name = [location['First Name'], location['Last Name']]
|
|
.filter(Boolean).join(' ') || 'Unknown';
|
|
const address = location.Address || 'No address';
|
|
const supportLevel = location['Support Level'] ?
|
|
`Level ${location['Support Level']}` : 'Not specified';
|
|
|
|
return `
|
|
<div class="popup-content">
|
|
<h3>${escapeHtml(name)}</h3>
|
|
<p><strong>Address:</strong> ${escapeHtml(address)}</p>
|
|
<p><strong>Support:</strong> ${escapeHtml(supportLevel)}</p>
|
|
${location.Sign ? '<p>🏁 Has campaign sign</p>' : ''}
|
|
${location.Notes ? `<p><strong>Notes:</strong> ${escapeHtml(location.Notes)}</p>` : ''}
|
|
<div class="popup-meta">
|
|
<p>ID: ${locationId || 'Unknown'}</p>
|
|
</div>
|
|
<div class="popup-actions">
|
|
<button class="btn btn-primary btn-sm edit-location-popup-btn"
|
|
data-location='${escapeHtml(JSON.stringify(location))}'>
|
|
✏️ Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Full information for regular users and admins
|
|
const name = [location['First Name'], location['Last Name']]
|
|
.filter(Boolean).join(' ') || 'Unknown';
|
|
const address = location.Address || 'No address';
|
|
const supportLevel = location['Support Level'] ?
|
|
`Level ${location['Support Level']}` : 'Not specified';
|
|
|
|
// Add debugging only for non-temp users
|
|
if (currentUser?.userType !== 'temp') {
|
|
console.log('Creating popup for location:', locationId, location);
|
|
}
|
|
|
|
return `
|
|
<div class="popup-content">
|
|
<h3>${escapeHtml(name)}</h3>
|
|
<p><strong>Address:</strong> ${escapeHtml(address)}</p>
|
|
<p><strong>Support:</strong> ${escapeHtml(supportLevel)}</p>
|
|
${location.Sign ? '<p>🏁 Has campaign sign</p>' : ''}
|
|
${location.Notes ? `<p><strong>Notes:</strong> ${escapeHtml(location.Notes)}</p>` : ''}
|
|
<div class="popup-meta">
|
|
<p>ID: ${locationId || 'Unknown'}</p>
|
|
</div>
|
|
${currentUser ? `
|
|
<div class="popup-actions">
|
|
<button class="btn btn-primary btn-sm edit-location-popup-btn"
|
|
data-location='${escapeHtml(JSON.stringify(location))}'>
|
|
✏️ Edit
|
|
</button>
|
|
${currentUser.userType !== 'temp' ? `
|
|
<button class="btn btn-primary btn-sm move-location-popup-btn"
|
|
data-location='${escapeHtml(JSON.stringify(location))}'>
|
|
📍 Move
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export async function handleAddLocation(e) {
|
|
e.preventDefault();
|
|
|
|
// Check if address is confirmed
|
|
const { getAddressConfirmationState } = await import('./ui-controls.js');
|
|
const { isAddressConfirmed } = getAddressConfirmationState();
|
|
if (!isAddressConfirmed) {
|
|
showStatus('Please confirm the address before saving the location', 'warning');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {};
|
|
|
|
// Convert form data to object
|
|
for (let [key, value] of formData.entries()) {
|
|
// Map form field names to NocoDB column names
|
|
if (key === 'latitude') data.latitude = value.trim();
|
|
else if (key === 'longitude') data.longitude = value.trim();
|
|
else if (key === 'Geo-Location') data['Geo-Location'] = value.trim();
|
|
else if (value.trim() !== '') {
|
|
data[key] = value.trim();
|
|
}
|
|
}
|
|
|
|
// Ensure geo-location is set
|
|
if (data.latitude && data.longitude) {
|
|
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
|
|
}
|
|
|
|
// Handle checkbox
|
|
data.Sign = document.getElementById('sign').checked;
|
|
|
|
try {
|
|
const response = await fetch('/api/locations', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showStatus('Location added successfully!', 'success');
|
|
closeAddModal();
|
|
loadLocations();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to add location');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding location:', error);
|
|
showStatus(error.message || 'Failed to add location', 'error');
|
|
}
|
|
}
|
|
|
|
export function openEditForm(location) {
|
|
currentEditingLocation = location;
|
|
|
|
// Extract ID - check multiple possible field names
|
|
const locationId = location.Id || location.id || location.ID || location._id;
|
|
|
|
if (!locationId) {
|
|
console.error('No ID found in location object. Available fields:', Object.keys(location));
|
|
showStatus('Error: Location ID not found. Check console for details.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Reset address confirmation state
|
|
resetAddressConfirmation('edit');
|
|
|
|
// Store the ID in a data attribute for later use
|
|
document.getElementById('edit-location-id').value = locationId;
|
|
document.getElementById('edit-location-id').setAttribute('data-location-id', locationId);
|
|
|
|
// Populate form fields
|
|
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-phone').value = location.Phone || '';
|
|
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 || '';
|
|
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-notes').value = location.Notes || '';
|
|
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'] || '';
|
|
|
|
// Show/hide delete button based on user type
|
|
const deleteBtn = document.getElementById('delete-location-btn');
|
|
if (deleteBtn) {
|
|
if (currentUser?.userType === 'temp') {
|
|
deleteBtn.style.display = 'none';
|
|
} else {
|
|
deleteBtn.style.display = '';
|
|
}
|
|
}
|
|
|
|
// Show edit footer
|
|
document.getElementById('edit-footer').classList.remove('hidden');
|
|
}
|
|
|
|
export function closeEditForm() {
|
|
document.getElementById('edit-footer').classList.add('hidden');
|
|
currentEditingLocation = null;
|
|
}
|
|
|
|
export async function handleEditLocation(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentEditingLocation) return;
|
|
|
|
// Check if address is confirmed
|
|
const { getAddressConfirmationState } = await import('./ui-controls.js');
|
|
const { isEditAddressConfirmed } = getAddressConfirmationState();
|
|
if (!isEditAddressConfirmed) {
|
|
showStatus('Please confirm the address before saving changes', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Get the stored location ID
|
|
const locationIdElement = document.getElementById('edit-location-id');
|
|
const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value;
|
|
|
|
if (!locationId || locationId === 'undefined') {
|
|
showStatus('Error: Location ID not found', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {};
|
|
|
|
// Convert form data to object
|
|
for (let [key, value] of formData.entries()) {
|
|
// Skip the ID field
|
|
if (key === 'id' || key === 'Id' || key === 'ID') continue;
|
|
|
|
if (value !== null && value !== undefined) {
|
|
data[key] = value.trim();
|
|
}
|
|
}
|
|
|
|
// Ensure geo-location is set
|
|
if (data.latitude && data.longitude) {
|
|
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
|
|
}
|
|
|
|
// Handle checkbox
|
|
data.Sign = document.getElementById('edit-sign').checked;
|
|
|
|
try {
|
|
const response = await fetch(`/api/locations/${locationId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const responseText = await response.text();
|
|
let result;
|
|
|
|
try {
|
|
result = JSON.parse(responseText);
|
|
} catch (e) {
|
|
console.error('Failed to parse response:', responseText);
|
|
throw new Error(`Server response error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
if (result.success) {
|
|
showStatus('Location updated successfully!', 'success');
|
|
closeEditForm();
|
|
loadLocations();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to update location');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating location:', error);
|
|
showStatus(`Update failed: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
export async function handleDeleteLocation() {
|
|
if (!currentEditingLocation) return;
|
|
|
|
// Get the stored location ID
|
|
const locationIdElement = document.getElementById('edit-location-id');
|
|
const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value;
|
|
|
|
if (!locationId || locationId === 'undefined') {
|
|
showStatus('Error: Location ID not found', 'error');
|
|
return;
|
|
}
|
|
|
|
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 (result.success) {
|
|
showStatus('Location deleted successfully!', 'success');
|
|
closeEditForm();
|
|
loadLocations();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to delete location');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting location:', error);
|
|
showStatus(error.message || 'Failed to delete location', 'error');
|
|
}
|
|
}
|
|
|
|
export function closeAddModal() {
|
|
const modal = document.getElementById('add-modal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
|
|
// Try to find and reset the form with multiple possible IDs
|
|
const form = document.getElementById('location-form') ||
|
|
document.getElementById('add-location-form');
|
|
if (form) {
|
|
form.reset();
|
|
}
|
|
}
|
|
|
|
export function openAddModal(lat, lng, performLookup = true) {
|
|
const modal = document.getElementById('add-modal');
|
|
|
|
if (!modal) {
|
|
console.error('Add modal not found');
|
|
return;
|
|
}
|
|
|
|
// Try multiple possible field IDs for coordinates
|
|
const latInput = document.getElementById('location-lat') ||
|
|
document.getElementById('add-latitude') ||
|
|
document.getElementById('latitude');
|
|
const lngInput = document.getElementById('location-lng') ||
|
|
document.getElementById('add-longitude') ||
|
|
document.getElementById('longitude');
|
|
const geoInput = document.getElementById('geo-location') ||
|
|
document.getElementById('add-geo-location') ||
|
|
document.getElementById('Geo-Location');
|
|
|
|
// Reset address confirmation state
|
|
resetAddressConfirmation('add');
|
|
|
|
// Set coordinates if input fields exist
|
|
if (latInput && lngInput) {
|
|
latInput.value = lat.toFixed(8);
|
|
lngInput.value = lng.toFixed(8);
|
|
}
|
|
|
|
if (geoInput) {
|
|
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
|
|
}
|
|
|
|
// Try to find and reset the form
|
|
const form = document.getElementById('location-form') ||
|
|
document.getElementById('add-location-form');
|
|
|
|
if (form) {
|
|
// Clear other fields but preserve coordinates
|
|
const tempLat = lat.toFixed(8);
|
|
const tempLng = lng.toFixed(8);
|
|
const tempGeo = `${tempLat};${tempLng}`;
|
|
|
|
form.reset();
|
|
|
|
// Restore coordinates after reset
|
|
if (latInput) latInput.value = tempLat;
|
|
if (lngInput) lngInput.value = tempLng;
|
|
if (geoInput) geoInput.value = tempGeo;
|
|
}
|
|
|
|
// Show modal
|
|
modal.classList.remove('hidden');
|
|
|
|
// Conditionally perform the auto address lookup
|
|
if (performLookup) {
|
|
const autoLookupEvent = new CustomEvent('autoAddressLookup', {
|
|
detail: { mode: 'add', 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;
|
|
if (marker.setStyle) { // It's a circleMarker
|
|
marker.setStyle({
|
|
fillColor: '#ff7800',
|
|
fillOpacity: 0.9,
|
|
radius: 10
|
|
});
|
|
} else if (marker.setIcon) { // It's a divIcon marker
|
|
originalIcon = marker.getIcon();
|
|
const movingIcon = L.divIcon({
|
|
className: 'multi-unit-marker moving',
|
|
html: `<div class="apartment-marker" style="background-color: #ff7800;"></div>`,
|
|
iconSize: [24, 24],
|
|
iconAnchor: [12, 12]
|
|
});
|
|
marker.setIcon(movingIcon);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="move-confirm-popup">
|
|
<h3>Confirm Move</h3>
|
|
<p>Move "${escapeHtml(name)}" to this location?</p>
|
|
<div class="popup-actions">
|
|
<button id="confirm-move-btn" class="btn btn-primary btn-sm">✓ Confirm</button>
|
|
<button id="cancel-move-btn" class="btn btn-secondary btn-sm">✕ Cancel</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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) {
|
|
if (movingMarker.setStyle) { // It's a circleMarker
|
|
// 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
|
|
});
|
|
} else if (movingMarker.setIcon && originalIcon) {
|
|
movingMarker.setIcon(originalIcon);
|
|
}
|
|
}
|
|
|
|
// Reset state
|
|
isMovingMarker = false;
|
|
movingMarker = null;
|
|
originalPosition = null;
|
|
movingLocationData = null;
|
|
originalIcon = null;
|
|
}
|
|
|
|
function createMultiUnitMarker(group) {
|
|
if (!map) {
|
|
console.warn('Map not initialized, skipping multi-unit marker creation');
|
|
return null;
|
|
}
|
|
|
|
const { lat, lng, locations, address } = group;
|
|
|
|
// Validate coordinates
|
|
if (!lat || !lng || isNaN(lat) || isNaN(lng)) {
|
|
console.warn('Invalid coordinates for multi-unit location:', group);
|
|
return null;
|
|
}
|
|
|
|
// Determine dominant support level for color coding
|
|
const supportLevelCounts = locations.reduce((acc, loc) => {
|
|
const level = loc['Support Level'];
|
|
if (level) {
|
|
acc[level] = (acc[level] || 0) + 1;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
|
|
let dominantSupportLevel = null;
|
|
let maxCount = 0;
|
|
for (const level in supportLevelCounts) {
|
|
if (supportLevelCounts[level] > maxCount) {
|
|
maxCount = supportLevelCounts[level];
|
|
dominantSupportLevel = level;
|
|
}
|
|
}
|
|
|
|
// Set marker color based on dominant support level
|
|
let markerColor = '#3388ff'; // Default blue, same as single markers
|
|
if (dominantSupportLevel) {
|
|
const level = parseInt(dominantSupportLevel);
|
|
switch(level) {
|
|
case 1: markerColor = '#27ae60'; break; // Green
|
|
case 2: markerColor = '#f1c40f'; break; // Yellow
|
|
case 3: markerColor = '#e67e22'; break; // Orange
|
|
case 4: markerColor = '#e74c3c'; break; // Red
|
|
}
|
|
}
|
|
|
|
// Create a square marker using DivIcon for apartment buildings
|
|
const icon = L.divIcon({
|
|
className: 'multi-unit-marker',
|
|
html: `<div class="apartment-marker" style="background-color: ${markerColor};"></div>`,
|
|
iconSize: [20, 20],
|
|
iconAnchor: [10, 10],
|
|
popupAnchor: [0, -10]
|
|
});
|
|
|
|
const marker = L.marker([lat, lng], {
|
|
icon: icon
|
|
});
|
|
|
|
// Add to map
|
|
marker.addTo(map);
|
|
|
|
// Create apartment-style popup content
|
|
const popupContent = createMultiUnitPopupContent(group);
|
|
const popup = L.popup({
|
|
maxWidth: 320,
|
|
minWidth: 280,
|
|
closeButton: true,
|
|
className: 'apartment-popup app-data'
|
|
}).setContent(popupContent);
|
|
|
|
marker.bindPopup(popup);
|
|
marker._locationData = group;
|
|
|
|
// Add event listener for when popup opens
|
|
marker.on('popupopen', function(e) {
|
|
setupAppApartmentPopupListeners(group);
|
|
});
|
|
|
|
// Only log multi-unit marker creation for non-temp users
|
|
if (currentUser?.userType !== 'temp') {
|
|
console.log(`Created multi-unit marker at ${lat}, ${lng} with ${locations.length} units`);
|
|
}
|
|
|
|
return marker;
|
|
}
|
|
|
|
function createMultiUnitPopupContent(group) {
|
|
const { address, locations } = group;
|
|
const totalUnits = locations.length;
|
|
|
|
// Sort locations by unit number if available
|
|
const sortedLocations = locations.sort((a, b) => {
|
|
const aUnit = a['Unit Number'] || '';
|
|
const bUnit = b['Unit Number'] || '';
|
|
|
|
// Extract numeric part for sorting
|
|
const aNum = aUnit ? parseInt(aUnit.replace(/\D/g, '')) || 0 : 0;
|
|
const bNum = bUnit ? parseInt(bUnit.replace(/\D/g, '')) || 0 : 0;
|
|
|
|
return aNum - bNum;
|
|
});
|
|
|
|
const popupId = `app-apartment-popup-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
// Truncate address if too long for mobile
|
|
const displayAddress = address.length > 30 ?
|
|
address.substring(0, 30) + '...' : address;
|
|
|
|
const popupContent = `
|
|
<div class="apartment-building-popup app-data" data-popup-id="${popupId}" style="font-family: Arial, sans-serif;">
|
|
<div class="building-header" style="background: linear-gradient(135deg, #a02c8d, #ba6cdf);">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px;">
|
|
<div style="flex: 1; min-width: 0;">
|
|
<div style="font-weight: bold; font-size: 15px; margin-bottom: 2px; line-height: 1.3;" title="${escapeHtml(address)}">🏢 ${escapeHtml(displayAddress)}</div>
|
|
<div style="font-size: 12px; opacity: 0.9; line-height: 1.2;">${totalUnits} contacts</div>
|
|
</div>
|
|
<div style="background: rgba(255,255,255,0.25); padding: 4px 8px; border-radius: 10px; font-size: 10px; font-weight: 500; white-space: nowrap;">
|
|
Multi-Unit
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="unit-navigator" style="margin-bottom: 12px;">
|
|
<div style="margin-bottom: 6px;">
|
|
<label style="font-size: 11px; color: #666; display: block; margin-bottom: 3px;">
|
|
Contact (${totalUnits} total):
|
|
</label>
|
|
<select class="unit-selector" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 11px; background: white; cursor: pointer;">
|
|
${sortedLocations.map((location, index) => {
|
|
const name = [location['First Name'], location['Last Name']].filter(Boolean).join(' ') || 'Unknown';
|
|
const unit = location['Unit Number'] || `Unit ${index + 1}`;
|
|
const optionText = `${unit} - ${name}`;
|
|
// Truncate option text for mobile
|
|
const truncatedText = optionText.length > 25 ?
|
|
optionText.substring(0, 25) + '...' : optionText;
|
|
return `<option value="${index}" title="${escapeHtml(optionText)}">${escapeHtml(truncatedText)}</option>`;
|
|
}).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="unit-content" style="min-height: 90px; padding: 8px; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #a02c8d;">
|
|
<div class="unit-details">
|
|
${createUnitDetailsHTML(sortedLocations[0], 0)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; font-size: 9px; color: #888; border-top: 1px solid #e9ecef; padding-top: 6px; margin-top: 10px;">
|
|
Your Campaign Database
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return popupContent;
|
|
}
|
|
|
|
function createUnitDetailsHTML(location, index) {
|
|
const locationId = location.Id || location.id || location.ID || location._id;
|
|
const name = [location['First Name'], location['Last Name']].filter(Boolean).join(' ') || 'Unknown';
|
|
const unit = location['Unit Number'] || `Unit ${index + 1}`;
|
|
const supportLevel = location['Support Level'] ? `Level ${location['Support Level']}` : 'Not specified';
|
|
|
|
// For temp users, hide sensitive contact information
|
|
const isTemp = currentUser?.userType === 'temp';
|
|
const email = !isTemp ? (location.Email || '') : '';
|
|
const phone = !isTemp ? (location.Phone || '') : '';
|
|
|
|
// Truncate long values for mobile
|
|
const truncatedEmail = email.length > 25 ? email.substring(0, 25) + '...' : email;
|
|
const truncatedPhone = phone.length > 15 ? phone.substring(0, 15) + '...' : phone;
|
|
const truncatedNotes = location.Notes && location.Notes.length > 50 ?
|
|
location.Notes.substring(0, 50) + '...' : location.Notes;
|
|
|
|
return `
|
|
<div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 3px;">
|
|
${escapeHtml(unit)} - ${escapeHtml(name)}
|
|
</div>
|
|
<div style="font-size: 11px; color: #666; margin-bottom: 4px;">
|
|
Support: ${escapeHtml(supportLevel)}
|
|
</div>
|
|
${email ? `
|
|
<div style="font-size: 10px; color: #888; margin-bottom: 3px;" title="${escapeHtml(email)}">
|
|
📧 ${escapeHtml(truncatedEmail)}
|
|
</div>` : ''}
|
|
${phone ? `
|
|
<div style="font-size: 10px; color: #888; margin-bottom: 3px;" title="${escapeHtml(phone)}">
|
|
📞 ${escapeHtml(truncatedPhone)}
|
|
</div>` : ''}
|
|
${location.Sign ? '<div style="font-size: 10px; color: #27ae60; margin-bottom: 3px;">🏁 Has sign</div>' : ''}
|
|
${location.Notes ? `
|
|
<div style="font-size: 9px; color: #888; margin-top: 4px; padding-top: 4px; border-top: 1px solid #e9ecef;" title="${escapeHtml(location.Notes)}">
|
|
${escapeHtml(truncatedNotes)}
|
|
</div>` : ''}
|
|
${currentUser ? `
|
|
<div style="margin-top: 6px; padding-top: 6px; border-top: 1px solid #e9ecef;">
|
|
<div style="display: flex; gap: 3px; flex-wrap: wrap;">
|
|
<button class="btn btn-primary btn-sm edit-unit-btn"
|
|
data-location='${escapeHtml(JSON.stringify(location))}'
|
|
style="background: #a02c8d; border: none; padding: 3px 6px; border-radius: 3px; font-size: 9px; cursor: pointer; flex: 1; min-width: 45%;">
|
|
✏️ Edit
|
|
</button>
|
|
${currentUser.userType !== 'temp' ? `
|
|
<button class="btn btn-primary btn-sm move-unit-btn"
|
|
data-location='${escapeHtml(JSON.stringify(location))}'
|
|
style="background: #6c757d; border: none; padding: 3px 6px; border-radius: 3px; font-size: 9px; cursor: pointer; flex: 1; min-width: 45%;">
|
|
📍 Move
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
<div style="font-size: 8px; color: #999; margin-top: 4px;">
|
|
ID: ${locationId || 'Unknown'}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function setupAppApartmentPopupListeners(group) {
|
|
const { locations } = group;
|
|
|
|
// Sort locations same as in popup creation
|
|
const sortedLocations = locations.sort((a, b) => {
|
|
const aUnit = a['Unit Number'] || '';
|
|
const bUnit = b['Unit Number'] || '';
|
|
const aNum = aUnit ? parseInt(aUnit.replace(/\D/g, '')) || 0 : 0;
|
|
const bNum = bUnit ? parseInt(bUnit.replace(/\D/g, '')) || 0 : 0;
|
|
return aNum - bNum;
|
|
});
|
|
|
|
// Find the popup container in the DOM
|
|
const container = document.querySelector('.apartment-building-popup.app-data');
|
|
if (!container) {
|
|
console.log('App apartment popup container not found');
|
|
return;
|
|
}
|
|
|
|
const unitSelector = container.querySelector('.unit-selector');
|
|
const unitDetails = container.querySelector('.unit-details');
|
|
|
|
if (!unitSelector || !unitDetails) {
|
|
console.log('App apartment popup elements not found');
|
|
return;
|
|
}
|
|
|
|
function updateUnitDisplay(selectedIndex) {
|
|
const location = sortedLocations[selectedIndex];
|
|
unitDetails.innerHTML = createUnitDetailsHTML(location, selectedIndex);
|
|
|
|
// Re-attach event listeners for the new buttons
|
|
setTimeout(() => {
|
|
attachUnitButtonListeners();
|
|
}, 10);
|
|
}
|
|
|
|
function attachUnitButtonListeners() {
|
|
const editBtn = container.querySelector('.edit-unit-btn');
|
|
const moveBtn = container.querySelector('.move-unit-btn');
|
|
|
|
if (editBtn) {
|
|
editBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const locationData = JSON.parse(editBtn.getAttribute('data-location'));
|
|
map.closePopup();
|
|
openEditForm(locationData);
|
|
});
|
|
}
|
|
|
|
if (moveBtn) {
|
|
moveBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const locationData = JSON.parse(moveBtn.getAttribute('data-location'));
|
|
map.closePopup();
|
|
// Find the marker for this location
|
|
const marker = markers.find(m => m._locationData &&
|
|
(m._locationData.locations ?
|
|
m._locationData.locations.some(loc =>
|
|
(loc.Id || loc.id || loc.ID || loc._id) === (locationData.Id || locationData.id || locationData.ID || locationData._id)
|
|
) :
|
|
(m._locationData.Id || m._locationData.id || m._locationData.ID || m._locationData._id) === (locationData.Id || locationData.id || locationData.ID || locationData._id)
|
|
)
|
|
);
|
|
if (marker) {
|
|
startMovingMarker(locationData, marker);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Set up unit selector dropdown event listener
|
|
unitSelector.addEventListener('change', (e) => {
|
|
e.stopPropagation();
|
|
const selectedIndex = parseInt(e.target.value);
|
|
console.log('Unit selected:', selectedIndex);
|
|
updateUnitDisplay(selectedIndex);
|
|
});
|
|
|
|
// Initialize with first unit selected and attach initial listeners
|
|
updateUnitDisplay(0);
|
|
|
|
console.log(`App apartment popup unit selector set up for ${sortedLocations.length} units`);
|
|
} |