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

444 lines
17 KiB
JavaScript

/**
* 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)
});
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: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
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 `
<div class="location-popup">
<h4>${name}</h4>
<p><strong>Address:</strong> ${address}</p>
<p><strong>Support Level:</strong> ${supportLevel}</p>
<p><strong>Lawn Sign:</strong> ${hasSign}</p>
${email ? `<p><strong>Email:</strong> ${email}</p>` : ''}
${phone ? `<p><strong>Phone:</strong> ${phone}</p>` : ''}
${notes ? `<p><strong>Notes:</strong> ${notes}</p>` : ''}
</div>
`;
}
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 '<div class="filters-info"><strong>Filters:</strong> None (showing all locations)</div>';
}
return `<div class="filters-info"><strong>Active Filters:</strong> ${activeFilters.join(', ')}</div>`;
}
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;
}