/** * Cut Location Management Module * Handles location display, filtering, statistics, and management for cuts */ class CutLocationManager { constructor(map, cutsManager) { this.map = map; this.cutsManager = cutsManager; this.currentCutId = null; this.currentCutLocations = []; this.currentFilters = {}; this.locationMarkersLayer = null; this.showingLocations = false; this.setupEventHandlers(); } setupEventHandlers() { // Toggle location visibility const toggleLocationBtn = document.getElementById('toggle-location-visibility'); if (toggleLocationBtn) { toggleLocationBtn.addEventListener('click', () => this.toggleLocationVisibility()); } // Export cut locations const exportBtn = document.getElementById('export-cut-locations'); if (exportBtn) { exportBtn.addEventListener('click', () => this.exportCutLocations()); } // Apply filters const applyFiltersBtn = document.getElementById('apply-filters'); if (applyFiltersBtn) { applyFiltersBtn.addEventListener('click', () => this.applyLocationFilters()); } // Clear filters const clearFiltersBtn = document.getElementById('clear-filters'); if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', () => this.clearLocationFilters()); } // Save cut settings const saveSettingsBtn = document.getElementById('save-cut-settings'); if (saveSettingsBtn) { saveSettingsBtn.addEventListener('click', () => this.saveCutSettings()); } } async toggleLocationVisibility() { if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } const toggleBtn = document.getElementById('toggle-location-visibility'); if (this.showingLocations) { // Hide locations if (this.locationMarkersLayer) { this.map.removeLayer(this.locationMarkersLayer); } this.showingLocations = false; toggleBtn.textContent = 'Show Locations'; toggleBtn.classList.remove('active'); toggleBtn.classList.add('inactive'); } else { // Show locations await this.loadCutLocations(); toggleBtn.textContent = 'Hide Locations'; toggleBtn.classList.add('active'); toggleBtn.classList.remove('inactive'); } } async loadCutLocations() { if (!this.currentCutId) return; try { const filters = this.getCurrentFilters(); const queryParams = new URLSearchParams(filters); const response = await fetch(`/api/cuts/${this.currentCutId}/locations?${queryParams}`, { credentials: 'include' }); if (!response.ok) { throw new Error(`Failed to load locations: ${response.statusText}`); } const data = await response.json(); this.currentCutLocations = data.locations || []; // Update statistics this.updateStatistics(data.statistics); // Display locations on map this.displayLocationsOnMap(this.currentCutLocations); this.showingLocations = true; this.showStatus(`Loaded ${this.currentCutLocations.length} locations`, 'success'); } catch (error) { console.error('Error loading cut locations:', error); this.showStatus('Failed to load locations', 'error'); } } displayLocationsOnMap(locations) { // Remove existing markers if (this.locationMarkersLayer) { this.map.removeLayer(this.locationMarkersLayer); } // Create new markers layer this.locationMarkersLayer = L.layerGroup(); locations.forEach(location => { if (location.latitude && location.longitude) { const marker = L.marker([location.latitude, location.longitude], { icon: this.createLocationIcon(location), zIndexOffset: 100 // Above City Data markers }); const popupContent = this.createLocationPopup(location); marker.bindPopup(popupContent); this.locationMarkersLayer.addLayer(marker); } }); this.locationMarkersLayer.addTo(this.map); } createLocationIcon(location) { // Create different icons based on support level const supportLevel = location.support_level || location['Support Level'] || 'unknown'; const colors = { '1': '#28a745', // Green - Strong support '2': '#ffc107', // Yellow - Lean support '3': '#fd7e14', // Orange - Lean opposition '4': '#dc3545', // Red - Strong opposition 'unknown': '#6c757d' // Grey - Unknown }; const color = colors[supportLevel] || colors['unknown']; return L.divIcon({ className: 'location-marker', html: `
`, iconSize: [16, 16], iconAnchor: [8, 8] }); } createLocationPopup(location) { // Handle different possible field names for NocoDB const firstName = location.first_name || location['First Name'] || ''; const lastName = location.last_name || location['Last Name'] || ''; const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown'; const address = location.address || location.Address || 'No address'; const supportLevel = location.support_level || location['Support Level'] || 'Unknown'; const hasSign = location.sign || location.Sign ? 'Yes' : 'No'; const email = location.email || location.Email || ''; const phone = location.phone || location.Phone || ''; const notes = location.notes || location.Notes || ''; return `

${name}

Address: ${address}

Support Level: ${supportLevel}

Lawn Sign: ${hasSign}

${email ? `

Email: ${email}

` : ''} ${phone ? `

Phone: ${phone}

` : ''} ${notes ? `

Notes: ${notes}

` : ''}
`; } getCurrentFilters() { return { support_level: document.getElementById('support-level-filter')?.value || '', has_sign: document.getElementById('sign-status-filter')?.value || '', sign_size: document.getElementById('sign-size-filter')?.value || '', contact_filter: document.getElementById('contact-filter')?.value || '' }; } async applyLocationFilters() { if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } await this.loadCutLocations(); } clearLocationFilters() { document.getElementById('support-level-filter').value = ''; document.getElementById('sign-status-filter').value = ''; document.getElementById('sign-size-filter').value = ''; document.getElementById('contact-filter').value = ''; if (this.currentCutId && this.showingLocations) { this.loadCutLocations(); } } updateStatistics(statistics) { if (!statistics) return; document.getElementById('total-locations').textContent = statistics.total_locations || 0; document.getElementById('support-1').textContent = statistics.support_levels?.['1'] || 0; document.getElementById('support-2').textContent = statistics.support_levels?.['2'] || 0; document.getElementById('has-signs').textContent = statistics.lawn_signs?.has_sign || 0; document.getElementById('has-email').textContent = statistics.contact_info?.has_email || 0; document.getElementById('has-phone').textContent = statistics.contact_info?.has_phone || 0; // Show statistics panel document.getElementById('cut-statistics').style.display = 'block'; } async exportCutLocations() { if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } try { const filters = this.getCurrentFilters(); const queryParams = new URLSearchParams(filters); const response = await fetch(`/api/cuts/${this.currentCutId}/locations/export?${queryParams}`, { credentials: 'include' }); if (!response.ok) { throw new Error(`Export failed: ${response.statusText}`); } // Download the CSV file const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; // Get filename from response header or use default const contentDisposition = response.headers.get('content-disposition'); const filename = contentDisposition ? contentDisposition.split('filename=')[1]?.replace(/"/g, '') : `cut_locations_${new Date().toISOString().split('T')[0]}.csv`; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); this.showStatus('Locations exported successfully', 'success'); } catch (error) { console.error('Error exporting locations:', error); this.showStatus('Failed to export locations', 'error'); } } async saveCutSettings() { if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } try { const settings = { show_locations: document.getElementById('show-locations-toggle').checked, export_enabled: document.getElementById('export-enabled-toggle').checked, assigned_to: document.getElementById('assigned-to').value, completion_percentage: parseInt(document.getElementById('completion-percentage').value) || 0 }; const response = await fetch(`/api/cuts/${this.currentCutId}/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(settings) }); if (!response.ok) { throw new Error(`Failed to save settings: ${response.statusText}`); } this.showStatus('Cut settings saved successfully', 'success'); } catch (error) { console.error('Error saving cut settings:', error); this.showStatus('Failed to save cut settings', 'error'); } } showLocationManagement(cutId) { console.log('showLocationManagement called with cutId:', cutId); this.currentCutId = cutId; const locationSection = document.getElementById('cut-location-management'); console.log('Location section element:', locationSection); if (locationSection) { console.log('Setting location section display to block'); locationSection.style.display = 'block'; // Load cut data to populate settings - use multiple possible ID field names for NocoDB const cut = this.cutsManager.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId ); console.log('Found cut for location management:', cut); if (cut) { const toggleElement = document.getElementById('show-locations-toggle'); const exportElement = document.getElementById('export-enabled-toggle'); const assignedElement = document.getElementById('assigned-to'); const completionElement = document.getElementById('completion-percentage'); console.log('Setting up toggles:', { toggleElement: !!toggleElement, exportElement: !!exportElement, assignedElement: !!assignedElement, completionElement: !!completionElement }); if (toggleElement) toggleElement.checked = (cut.show_locations || cut.Show_locations || cut['Show Locations']) !== false; if (exportElement) exportElement.checked = (cut.export_enabled || cut.Export_enabled || cut['Export Enabled']) !== false; if (assignedElement) assignedElement.value = cut.assigned_to || cut.Assigned_to || cut['Assigned To'] || ''; if (completionElement) completionElement.value = cut.completion_percentage || cut.Completion_percentage || cut['Completion Percentage'] || ''; } } else { console.error('Location management section not found!'); } } hideLocationManagement() { const locationSection = document.getElementById('cut-location-management'); if (locationSection) { locationSection.style.display = 'none'; } // Clear current data this.currentCutId = null; this.currentCutLocations = []; // Hide locations on map if (this.locationMarkersLayer) { this.map.removeLayer(this.locationMarkersLayer); } this.showingLocations = false; // Reset button state const toggleBtn = document.getElementById('toggle-location-visibility'); if (toggleBtn) { toggleBtn.textContent = 'Show Locations'; toggleBtn.classList.remove('active'); toggleBtn.classList.remove('inactive'); } } showStatus(message, type) { // Use existing admin notification system if available if (typeof showNotification === 'function') { showNotification(message, type); } else if (this.cutsManager && this.cutsManager.showStatus) { this.cutsManager.showStatus(message, type); } else { console.log(`[${type.toUpperCase()}] ${message}`); } } getSupportColor(supportLevel) { switch(supportLevel) { case '1': return '#28a745'; // Strong Support - Green case '2': return '#ffc107'; // Lean Support - Yellow case '3': return '#fd7e14'; // Oppose - Orange case '4': return '#dc3545'; // Strong Oppose - Red default: return '#6c757d'; // Unknown - Gray } } getCurrentFiltersDisplay() { const activeFilters = []; // Check location filters const supportFilter = document.getElementById('support-filter')?.value; if (supportFilter && supportFilter !== 'all') { activeFilters.push(`Support Level: ${supportFilter}`); } const signFilter = document.getElementById('sign-filter')?.value; if (signFilter && signFilter !== 'all') { activeFilters.push(`Lawn Signs: ${signFilter === 'true' ? 'Yes' : 'No'}`); } const contactFilter = document.getElementById('contact-filter')?.value; if (contactFilter && contactFilter !== 'all') { const contactLabels = { 'email': 'Has Email', 'phone': 'Has Phone', 'both': 'Has Both Email & Phone', 'none': 'No Contact Info' }; activeFilters.push(`Contact: ${contactLabels[contactFilter] || contactFilter}`); } if (activeFilters.length === 0) { return '
Filters: None (showing all locations)
'; } return `
Active Filters: ${activeFilters.join(', ')}
`; } getActiveFiltersCount() { let count = 0; const supportFilter = document.getElementById('support-filter')?.value; if (supportFilter && supportFilter !== 'all') count++; const signFilter = document.getElementById('sign-filter')?.value; if (signFilter && signFilter !== 'all') count++; const contactFilter = document.getElementById('contact-filter')?.value; if (contactFilter && contactFilter !== 'all') count++; return count; } } // Export the class if using modules, otherwise it's global if (typeof module !== 'undefined' && module.exports) { module.exports = CutLocationManager; } else { window.CutLocationManager = CutLocationManager; }