freealberta/map/app/public/js/location-manager.js

1008 lines
36 KiB
JavaScript

// Helper function to escape HTML to prevent XSS
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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 {
const response = await fetch('/api/locations');
const data = await response.json();
if (data.success) {
displayLocations(data.locations);
updateLocationCount(data.locations.length);
} else {
throw new Error(data.error || 'Failed to load locations');
}
} 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);
}
}
});
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;
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;
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
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>
<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 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');
modal.classList.add('hidden');
document.getElementById('location-form').reset();
}
export function openAddModal(lat, lng, performLookup = true) {
const modal = document.getElementById('add-modal');
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const geoInput = document.getElementById('geo-location');
// Reset address confirmation state
resetAddressConfirmation('add');
// Set coordinates
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// Clear other fields
document.getElementById('location-form').reset();
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// 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);
});
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';
const email = location.Email || '';
const phone = 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>
<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`);
}