445 lines
17 KiB
JavaScript
445 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),
|
|
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: `<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;
|
|
}
|