/** * Admin Cuts Management Module * Handles cut creation, editing, and management in the admin panel */ // Cut Drawing Class (converted to regular JS) class CutDrawing { constructor(map) { this.map = map; this.vertices = []; this.markers = []; this.polyline = null; this.polygon = null; // Add polygon preview this.finalPolygon = null; // Final polygon after finishing this.isDrawing = false; this.onFinishCallback = null; this.onCancelCallback = null; this.currentColor = '#3388ff'; this.currentOpacity = 0.3; } startDrawing(onFinish, onCancel) { if (this.isDrawing) { this.cancelDrawing(); } this.isDrawing = true; this.onFinishCallback = onFinish; this.onCancelCallback = onCancel; this.vertices = []; this.markers = []; this.map.getContainer().style.cursor = 'crosshair'; this.map.on('click', this.onMapClick.bind(this)); this.map.doubleClickZoom.disable(); console.log('Cut drawing started - click to add points'); } onMapClick(e) { if (!this.isDrawing) return; // Check if clicking on first vertex to close polygon if (this.vertices.length >= 3) { const firstVertex = this.vertices[0]; const clickPoint = this.map.latLngToContainerPoint(e.latlng); const firstPoint = this.map.latLngToContainerPoint(firstVertex); const distance = Math.sqrt( Math.pow(clickPoint.x - firstPoint.x, 2) + Math.pow(clickPoint.y - firstPoint.y, 2) ); // If clicked within 15 pixels of first vertex, close the polygon if (distance < 15) { console.log('Closing polygon by clicking first vertex'); this.finishDrawing(); return; } } // Add vertex marker with special styling for first vertex const isFirstVertex = this.vertices.length === 0; const marker = L.marker(e.latlng, { icon: L.divIcon({ className: 'cut-vertex-marker' + (isFirstVertex ? ' first-vertex' : ''), html: `
`, iconSize: [12, 12], iconAnchor: [6, 6] }), draggable: false }).addTo(this.map); // Add tooltip to first vertex after 3 points if (isFirstVertex) { marker.bindTooltip('Click to close polygon', { permanent: false, direction: 'top', offset: [0, -10] }); } this.vertices.push(e.latlng); this.markers.push(marker); this.updatePolyline(); // Show tooltip on first vertex when we have enough points if (this.vertices.length === 3 && this.markers[0]) { this.markers[0].openTooltip(); } // Call update callback if available if (this.onUpdate) { this.onUpdate(); } console.log(`Added vertex ${this.vertices.length} at`, e.latlng); } updatePolyline() { // Use stored color and opacity values const color = this.currentColor; const opacity = this.currentOpacity; if (this.polyline) { this.map.removeLayer(this.polyline); } if (this.polygon) { this.map.removeLayer(this.polygon); this.polygon = null; } if (this.vertices.length > 1) { // Show polyline for incomplete polygon this.polyline = L.polyline(this.vertices, { color: color, weight: 2, dashArray: '5, 5', opacity: 1.0 // Keep polyline stroke visible }).addTo(this.map); // Show preview polygon if we have 3+ vertices if (this.vertices.length >= 3) { this.polygon = L.polygon(this.vertices, { color: color, fillColor: color, fillOpacity: opacity, weight: 2, opacity: 0.8, dashArray: '5, 5' }).addTo(this.map); // Add cut-polygon class and force inline style if (this.polygon._path) { this.polygon._path.classList.add('cut-polygon'); // Use setProperty with important flag for stronger override this.polygon._path.style.setProperty('fill-opacity', opacity, 'important'); this.polygon._path.style.setProperty('fill', color, 'important'); console.log(`Created/updated polygon with opacity: ${opacity}`); } console.log('Created/updated polygon with opacity:', opacity); } } } finishDrawing() { console.log('finishDrawing() called'); console.log('isDrawing:', this.isDrawing); console.log('vertices count:', this.vertices.length); if (!this.isDrawing) { console.log('Not in drawing mode, returning null'); return null; } if (this.vertices.length < 3) { alert('A cut must have at least 3 points. Click more points or cancel drawing.'); return null; } // Store vertices before cleanup const finalVertices = [...this.vertices]; // Use stored color and opacity values const color = this.currentColor; const opacity = this.currentOpacity; // Create polygon from vertices const polygon = L.polygon(finalVertices, { color: color, fillColor: color, fillOpacity: opacity, weight: 2, opacity: 1.0 // Keep stroke visible }); // Get GeoJSON and bounds const geojson = polygon.toGeoJSON().geometry; // Get just the geometry part const bounds = polygon.getBounds(); // Clean up drawing elements but keep the final polygon this.cleanupDrawingElements(); // Add the final polygon to the map this.finalPolygon = polygon.addTo(this.map); // Call finish callback with the data console.log('Calling finish callbacks...'); const callbackData = { geojson: JSON.stringify(geojson), bounds: JSON.stringify([ [bounds.getSouth(), bounds.getWest()], [bounds.getNorth(), bounds.getEast()] ]), vertexCount: finalVertices.length, polygon: this.finalPolygon }; if (this.onFinishCallback) { console.log('Calling onFinishCallback'); this.onFinishCallback(callbackData); } else { console.log('No onFinishCallback set'); } // Also call the onFinish callback if set if (this.onFinish) { console.log('Calling onFinish'); this.onFinish(callbackData); } else { console.log('No onFinish callback set'); } console.log('Cut drawing finished with', finalVertices.length, 'vertices'); return geojson; } cancelDrawing() { if (!this.isDrawing) return; console.log('Cut drawing cancelled'); this.cleanup(); if (this.onCancelCallback) { this.onCancelCallback(); } // Also call the onCancel callback if set if (this.onCancel) { this.onCancel(); } } undoLastVertex() { if (!this.isDrawing || this.vertices.length === 0) return; this.vertices.pop(); const lastMarker = this.markers.pop(); if (lastMarker) { this.map.removeLayer(lastMarker); } this.updatePolyline(); // Call update callback if available if (this.onUpdate) { this.onUpdate(); } console.log('Removed last vertex, remaining:', this.vertices.length); } clearVertices() { if (!this.isDrawing) return; this.markers.forEach(marker => { this.map.removeLayer(marker); }); if (this.polyline) { this.map.removeLayer(this.polyline); this.polyline = null; } if (this.polygon) { this.map.removeLayer(this.polygon); this.polygon = null; } this.vertices = []; this.markers = []; // Call update callback if available if (this.onUpdate) { this.onUpdate(); } console.log('Cleared all vertices'); } cleanupDrawingElements() { // Remove drawing elements but preserve final polygon this.markers.forEach(marker => { this.map.removeLayer(marker); }); if (this.polyline) { this.map.removeLayer(this.polyline); } if (this.polygon) { this.map.removeLayer(this.polygon); } this.map.getContainer().style.cursor = ''; this.map.off('click', this.onMapClick); this.map.doubleClickZoom.enable(); this.isDrawing = false; this.vertices = []; this.markers = []; this.polyline = null; this.polygon = null; this.onFinishCallback = null; this.onCancelCallback = null; } cleanup() { this.markers.forEach(marker => { this.map.removeLayer(marker); }); if (this.polyline) { this.map.removeLayer(this.polyline); } if (this.polygon) { this.map.removeLayer(this.polygon); } if (this.finalPolygon) { this.map.removeLayer(this.finalPolygon); } this.map.getContainer().style.cursor = ''; this.map.off('click', this.onMapClick); this.map.doubleClickZoom.enable(); this.isDrawing = false; this.vertices = []; this.markers = []; this.polyline = null; this.polygon = null; this.finalPolygon = null; this.onFinishCallback = null; this.onCancelCallback = null; } getState() { return { isDrawing: this.isDrawing, vertexCount: this.vertices.length, canFinish: this.vertices.length >= 3 }; } // Add method to update current drawing style updateDrawingStyle(color, opacity) { this.currentColor = color; this.currentOpacity = opacity; console.log(`CutDrawing.updateDrawingStyle called with color: ${color}, opacity: ${opacity}`); // Update polyline color if (this.polyline) { this.polyline.setStyle({ color: color }); } // Update polygon if it exists if (this.polygon) { this.polygon.setStyle({ color: color, fillColor: color, fillOpacity: opacity, opacity: 0.8 // Border opacity }); // Force inline style update and browser reflow if (this.polygon._path) { // Use setProperty with important flag for stronger override this.polygon._path.style.setProperty('fill-opacity', opacity, 'important'); this.polygon._path.style.setProperty('fill', color, 'important'); // Force multiple reflows to ensure update void this.polygon._path.offsetHeight; void this.polygon._path.offsetWidth; // Force repaint by temporarily changing a property const oldDisplay = this.polygon._path.style.display; this.polygon._path.style.display = 'none'; void this.polygon._path.offsetHeight; this.polygon._path.style.display = oldDisplay; } // Also try Leaflet's internal redraw if (this.polygon._updatePath) { this.polygon._updatePath(); } console.log(`Updated active drawing polygon with opacity: ${opacity}`); } else { // If no polygon exists but we have vertices, force a complete redraw console.log('No polygon exists, forcing complete redraw'); this.forceRedraw(); } } clearPreview() { // Clear any preview polygons but not the final polygon if (this.polygon) { this.map.removeLayer(this.polygon); this.polygon = null; } if (this.polyline && this.isDrawing) { this.map.removeLayer(this.polyline); this.polyline = null; } } // Force a complete redraw with current style settings forceRedraw() { if (this.vertices.length > 1) { console.log('Forcing complete redraw with vertices:', this.vertices.length); this.updatePolyline(); } } } // Admin Cuts Manager Class class AdminCutsManager { constructor() { this.cutsMap = null; this.cutDrawing = null; this.currentCutId = null; this.allCuts = []; this.filteredCuts = []; this.currentCutLayer = null; this.isInitialized = false; // Add initialization flag // Location management properties this.currentCutLocations = []; this.currentFilters = {}; this.locationMarkersLayer = null; this.showingLocations = false; // Bind event handler once to avoid issues with removing listeners this.boundHandleCutActionClick = this.handleCutActionClick.bind(this); } async initialize() { // Prevent double initialization if (this.isInitialized) { console.log('AdminCutsManager already initialized'); return; } console.log('Initializing admin cuts manager...'); // Initialize map first this.initializeMap(); // Initialize form first this.initializeFormState(); // Initialize cuts list element this.cutsList = document.getElementById('cuts-list'); // Initialize drawing this.initializeDrawing(); // Load existing cuts await this.loadCuts(); // Set initialized flag BEFORE logging to prevent re-entry this.isInitialized = true; console.log('Admin cuts manager initialized'); } initializeFormState() { console.log('Initializing form state...'); // Set up form elements this.form = document.getElementById('cut-form'); if (!this.form) { console.error('Cut form not found'); return; } // Keep form enabled at all times - users should be able to fill properties anytime // Only disable the save button until we have geometry const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = true; } // Set up form submission this.form.addEventListener('submit', (e) => this.handleFormSubmit(e)); // NOTE: Opacity event listener is set up in setupFormControls() to avoid conflicts // Set up other form controls this.setupFormControls(); // Set up toolbar controls for drawing this.setupToolbarControls(); console.log('Form state initialized - form inputs enabled, save button disabled until geometry complete'); } // Add method to update preview style when color/opacity changes updatePreviewStyle() { // Simplified update method - uses toolbar values console.log('Note: updatePreviewStyle() called but now using toolbar controls'); // This method is no longer needed since toolbar controls update drawing style directly } // Method that accepts direct values to avoid DOM reading issues (kept for compatibility) updatePreviewStyleWithValues(colorOverride = null, opacityOverride = null) { const color = colorOverride || this.getCurrentColor(); const opacity = opacityOverride !== null ? opacityOverride : this.getCurrentOpacity(); console.log('updatePreviewStyleWithValues called with:', color, opacity); // Update drawing style if drawing is active if (this.cutDrawing) { this.cutDrawing.updateDrawingStyle(color, opacity); } } // Method to apply styles to all relevant layers applyStyleToLayers(color, opacity) { console.log('applyStyleToLayers called with color:', color, 'opacity:', opacity); // First, update the drawing tool's current style if drawing is active if (this.cutDrawing) { this.cutDrawing.updateDrawingStyle(color, opacity); } // Update any preview layer (GeoJSON) - this is the critical fix if (this.previewLayer) { this.updateLayerStyle(this.previewLayer, color, opacity); console.log('Preview GeoJSON layer style updated with opacity:', opacity); } // Update current cut layer if it exists - Enhanced handling if (this.currentCutLayer) { this.updateLayerStyle(this.currentCutLayer, color, opacity); console.log('Updated currentCutLayer with opacity:', opacity); } // If preview layer doesn't exist but we have drawing data, refresh the preview if (!this.previewLayer && this.currentDrawingData) { console.log('No preview layer found, refreshing preview with drawing data'); this.updateDrawingPreview(this.currentDrawingData); } } // New unified method to update any layer style updateLayerStyle(layer, color, opacity) { if (!layer) return; // Update layer options layer.options.fillOpacity = opacity; layer.options.fillColor = color; layer.options.color = color; // Apply new style layer.setStyle({ fillColor: color, color: color, fillOpacity: opacity, opacity: 0.8 // Border opacity }); // Force update on the path element(s) if (layer._path) { layer._path.style.setProperty('fill-opacity', opacity, 'important'); layer._path.style.setProperty('fill', color, 'important'); layer._path.style.setProperty('stroke', color, 'important'); // Force browser to recognize the change layer._path.style.opacity = ''; // Clear any overall opacity void layer._path.offsetHeight; // Force reflow } // Handle renderer sub-layers if (layer._renderer && layer._renderer._container) { const paths = layer._renderer._container.querySelectorAll('path'); paths.forEach(path => { path.style.setProperty('fill-opacity', opacity, 'important'); path.style.setProperty('fill', color, 'important'); path.style.setProperty('stroke', color, 'important'); // Force browser to recognize the change path.style.opacity = ''; // Clear any overall opacity void path.offsetHeight; // Force reflow }); } // Handle GeoJSON layers and layer groups if (layer.eachLayer) { // It's a layer group or GeoJSON - iterate through each feature layer.eachLayer((subLayer) => { if (subLayer.setStyle) { subLayer.setStyle({ color: color, fillColor: color, fillOpacity: opacity, opacity: 0.8 }); // Force inline styles on sub-layer paths if (subLayer._path) { subLayer._path.style.setProperty('fill-opacity', opacity, 'important'); subLayer._path.style.setProperty('fill', color, 'important'); subLayer._path.style.setProperty('stroke', color, 'important'); subLayer._path.style.opacity = ''; void subLayer._path.offsetHeight; } // Add CSS class to identify cut polygons const pathElement = subLayer.getElement(); if (pathElement) { pathElement.classList.add('cut-polygon'); pathElement.style.setProperty('fill-opacity', opacity, 'important'); console.log('Added cut-polygon class to sub-layer path element'); } // Force DOM update immediately this.forceLayerRedraw(subLayer); } }); // Also force a redraw of the entire layer group if (layer.redraw) { layer.redraw(); } } else if (layer.setStyle) { // It's a single layer (Leaflet Polygon) layer.setStyle({ fillColor: color, color: color, fillOpacity: opacity, opacity: 0.8 }); // Add CSS class to identify cut polygons const pathElement = layer.getElement(); if (pathElement) { pathElement.classList.add('cut-polygon'); console.log('Added cut-polygon class to single layer path element'); } // Force DOM update immediately this.forceLayerRedraw(layer); } } // New method to force layer redraw - addresses browser rendering issues forceLayerRedraw(layer) { if (layer._path) { // Direct SVG path manipulation for immediate visual update const path = layer._path; const targetOpacity = layer.options.fillOpacity; const targetColor = layer.options.fillColor || layer.options.color; console.log('forceLayerRedraw called:'); console.log(' - layer.options.fillOpacity:', targetOpacity); console.log(' - layer.options.fillColor:', targetColor); console.log(' - layer._path exists:', !!path); // Set the attribute directly on the SVG element path.setAttribute('fill-opacity', targetOpacity); path.setAttribute('fill', targetColor); // Also try setting as CSS style with important flag for better browser compatibility path.style.setProperty('fill-opacity', targetOpacity, 'important'); path.style.setProperty('fill', targetColor, 'important'); // Force browser reflow by temporarily changing a property const originalDisplay = path.style.display; path.style.display = 'none'; // Use requestAnimationFrame for better timing requestAnimationFrame(() => { path.style.display = originalDisplay; // Double-check the attribute was set const finalOpacity = path.getAttribute('fill-opacity'); const finalColor = path.getAttribute('fill'); const styleOpacity = path.style.fillOpacity; console.log('forceLayerRedraw completed:'); console.log(' - target opacity:', targetOpacity); console.log(' - target color:', targetColor); console.log(' - SVG attr opacity result:', finalOpacity); console.log(' - SVG attr color result:', finalColor); console.log(' - CSS style opacity result:', styleOpacity); // If attributes don't match, try one more time if (finalOpacity !== targetOpacity.toString()) { path.setAttribute('fill-opacity', targetOpacity); path.style.setProperty('fill-opacity', targetOpacity, 'important'); console.log(' - Re-applied fill-opacity attribute and style'); } }); } else { console.log('forceLayerRedraw: no _path found on layer'); } } setupFormControls() { // Set up start drawing button const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { // Remove any existing listeners first startDrawingBtn.removeEventListener('click', this.boundHandleStartDrawing); // Create bound method if it doesn't exist if (!this.boundHandleStartDrawing) { this.boundHandleStartDrawing = this.handleStartDrawing.bind(this); } startDrawingBtn.addEventListener('click', this.boundHandleStartDrawing); } // Set up reset form button const resetFormBtn = document.getElementById('reset-form-btn'); if (resetFormBtn) { resetFormBtn.addEventListener('click', () => this.resetForm()); } // Set up cancel edit button const cancelEditBtn = document.getElementById('cancel-edit-btn'); if (cancelEditBtn) { cancelEditBtn.addEventListener('click', () => this.cancelEdit()); } // Set up refresh cuts button const refreshCutsBtn = document.getElementById('refresh-cuts-btn'); if (refreshCutsBtn) { refreshCutsBtn.addEventListener('click', () => this.loadCuts()); } // Set up export button const exportCutsBtn = document.getElementById('export-cuts-btn'); if (exportCutsBtn) { exportCutsBtn.addEventListener('click', () => this.exportCuts()); } // Set up import file input const importCutsFile = document.getElementById('import-cuts-file'); if (importCutsFile) { importCutsFile.addEventListener('change', (e) => this.handleImport(e)); } // Set up search and filter const searchInput = document.getElementById('cuts-search'); if (searchInput) { searchInput.addEventListener('input', () => this.filterCuts()); } const categoryFilter = document.getElementById('cuts-category-filter'); if (categoryFilter) { categoryFilter.addEventListener('change', () => this.filterCuts()); } // NOTE: Color and opacity controls moved to toolbar for real-time feedback // Form-based color/opacity controls are no longer used // Add drawing toolbar button handlers const finishDrawingBtn = document.getElementById('finish-cut-btn'); // Fixed ID if (finishDrawingBtn) { finishDrawingBtn.addEventListener('click', () => { console.log('Finish button clicked'); if (this.cutDrawing) { console.log('Cut drawing exists, calling finishDrawing()'); console.log('Drawing state:', this.cutDrawing.getState()); this.cutDrawing.finishDrawing(); } else { console.error('Cut drawing not initialized'); } }); } else { console.error('Finish drawing button not found'); } const undoVertexBtn = document.getElementById('undo-vertex-btn'); if (undoVertexBtn) { undoVertexBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.undoLastVertex(); this.updateDrawingUI(); } }); } const clearVerticesBtn = document.getElementById('clear-vertices-btn'); if (clearVerticesBtn) { clearVerticesBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.clearVertices(); this.updateDrawingUI(); } }); } const cancelDrawingBtn = document.getElementById('cancel-cut-btn'); // Fixed ID if (cancelDrawingBtn) { cancelDrawingBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.cancelDrawing(); } }); } // Location management event listeners this.setupLocationManagementControls(); } // Set up toolbar controls for real-time drawing feedback setupToolbarControls() { const colorPicker = document.getElementById('toolbar-color'); const opacitySlider = document.getElementById('toolbar-opacity'); const opacityDisplay = document.getElementById('toolbar-opacity-display'); console.log('Setting up toolbar controls...', { colorPicker: !!colorPicker, opacitySlider: !!opacitySlider, opacityDisplay: !!opacityDisplay }); if (colorPicker) { colorPicker.addEventListener('input', (e) => { const color = e.target.value; console.log('Toolbar color changed to:', color); // Update drawing style immediately if (this.cutDrawing) { const opacity = this.getCurrentOpacity(); console.log('Updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } }); } if (opacitySlider && opacityDisplay) { opacitySlider.addEventListener('input', (e) => { const opacity = parseFloat(e.target.value); const percentage = Math.round(opacity * 100); opacityDisplay.textContent = percentage + '%'; console.log('Toolbar opacity changed to:', opacity, 'percentage:', percentage); // Update drawing style immediately if (this.cutDrawing) { const color = this.getCurrentColor(); console.log('Updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } else { console.warn('cutDrawing instance not available'); } }); } console.log('Toolbar controls setup complete'); } // Helper methods to get current toolbar values getCurrentColor() { const colorPicker = document.getElementById('toolbar-color'); return colorPicker ? colorPicker.value : '#3388ff'; } getCurrentOpacity() { const opacitySlider = document.getElementById('toolbar-opacity'); return opacitySlider ? parseFloat(opacitySlider.value) : 0.3; } // Force update the drawing style - useful for debugging or manual refresh forceUpdateDrawingStyle() { if (this.cutDrawing) { const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('Force updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } else { console.warn('cutDrawing instance not available for force update'); } } // Sync toolbar display values with actual slider values syncToolbarDisplayValues() { const opacitySlider = document.getElementById('toolbar-opacity'); const opacityDisplay = document.getElementById('toolbar-opacity-display'); if (opacitySlider && opacityDisplay) { const opacity = parseFloat(opacitySlider.value); const percentage = Math.round(opacity * 100); opacityDisplay.textContent = percentage + '%'; console.log('Synced opacity display to:', percentage + '%'); } // Also sync any existing preview layers with current toolbar values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); if (this.previewLayer) { console.log('Syncing preview layer with toolbar values:', color, opacity); this.updateLayerStyle(this.previewLayer, color, opacity); } if (this.currentCutLayer) { console.log('Syncing current cut layer with toolbar values:', color, opacity); this.updateLayerStyle(this.currentCutLayer, color, opacity); } } // Remove the form disable/enable methods since we keep form enabled at all times updateDrawingUI() { if (!this.cutDrawing) return; const state = this.cutDrawing.getState(); const vertexCount = document.getElementById('vertex-count'); const finishBtn = document.getElementById('finish-cut-btn'); // Fixed ID const undoBtn = document.getElementById('undo-vertex-btn'); if (vertexCount) { vertexCount.textContent = `${state.vertexCount} points`; } if (finishBtn) { finishBtn.disabled = !state.canFinish; } if (undoBtn) { undoBtn.disabled = state.vertexCount === 0; } } async initializeMap() { const mapContainer = document.getElementById('cuts-map'); if (!mapContainer) { console.error('Cuts map container not found'); return; } // Check if map is already initialized if (this.cutsMap) { console.log('Cuts map already initialized'); return; } // Check if container already has a map instance if (mapContainer._leaflet_id) { console.log('Map container already has a Leaflet instance, cleaning up...'); // Try to find and remove existing map const existingMap = L.map.hasOwnProperty(mapContainer._leaflet_id) ? L.map[mapContainer._leaflet_id] : null; if (existingMap) { existingMap.remove(); } delete mapContainer._leaflet_id; } // Initialize map this.cutsMap = L.map('cuts-map').setView([53.5461, -113.4938], 11); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.cutsMap); console.log('Cuts map initialized'); } initializeDrawing() { this.cutDrawing = new CutDrawing(this.cutsMap); // Ensure clean state this.cutDrawing.isDrawing = false; // Bind drawing events this.cutDrawing.onFinish = (data) => this.handleDrawingFinished(data); this.cutDrawing.onCancel = () => this.handleDrawingCancelled(); this.cutDrawing.onUpdate = () => this.updateDrawingUI(); console.log('Drawing initialized, isDrawing:', this.cutDrawing.isDrawing); } handleStartDrawing() { console.log('handleStartDrawing called, current drawing state:', this.cutDrawing.isDrawing); // Prevent double-click issues by adding a small delay check if (this.handleStartDrawing._processing) { console.log('Already processing start drawing, ignoring...'); return; } this.handleStartDrawing._processing = true; try { if (this.cutDrawing.isDrawing) { console.log('Already drawing, canceling...'); this.cutDrawing.cancelDrawing(); return; } console.log('Starting new drawing...'); // Get current toolbar values instead of form values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('Starting new drawing with color:', color, 'opacity:', opacity); // Update the drawing tool with current style if (this.cutDrawing) { this.cutDrawing.currentColor = color; this.cutDrawing.currentOpacity = opacity; // Force update the drawing style immediately this.cutDrawing.updateDrawingStyle(color, opacity); } // Clear any existing preview layers if (this.previewLayer) { this.cutsMap.removeLayer(this.previewLayer); this.previewLayer = null; } // Clear any existing current cut layer if (this.currentCutLayer) { if (this.currentCutLayer.remove) { this.currentCutLayer.remove(); } else { this.cutsMap.removeLayer(this.currentCutLayer); } this.currentCutLayer = null; } // Clear form data this.currentDrawingData = null; document.getElementById('cut-geojson').value = ''; document.getElementById('cut-bounds').value = ''; // Clear any existing preview from the drawing tool if (this.cutDrawing && this.cutDrawing.clearPreview) { this.cutDrawing.clearPreview(); } // Update button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Cancel Drawing'; } this.cutDrawing.startDrawing( (data) => this.handleDrawingFinished(data), () => this.handleDrawingCancelled() ); // Show drawing toolbar const toolbar = document.getElementById('cut-drawing-toolbar'); if (toolbar) { toolbar.classList.add('active'); // Sync toolbar display values and apply to any existing layers this.syncToolbarDisplayValues(); } // Update UI this.updateDrawingUI(); } finally { // Clear the processing flag after a short delay setTimeout(() => { this.handleStartDrawing._processing = false; }, 100); } } handleDrawingFinished(drawingData) { console.log('handleDrawingFinished() called with data:', drawingData); // Store the drawing data this.currentDrawingData = drawingData; // Hide drawing toolbar document.getElementById('cut-drawing-toolbar').classList.remove('active'); // Store geojson and bounds in hidden form fields document.getElementById('cut-geojson').value = drawingData.geojson; document.getElementById('cut-bounds').value = drawingData.bounds; // Store the geojson in form dataset for form submission const form = document.getElementById('cut-form'); if (form) { form.dataset.geojson = drawingData.geojson; form.dataset.bounds = drawingData.bounds; } // Store the polygon reference for later use if (drawingData.polygon) { this.currentCutLayer = drawingData.polygon; } // Update form title const titleElement = document.getElementById('cut-form-title'); if (titleElement) { titleElement.textContent = 'Cut Properties - Ready to Save'; } // Enable the save button now that we have geometry const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = false; } // Update start drawing button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Redraw Polygon'; } // Update preview with the drawn polygon using current toolbar values this.updateDrawingPreview(drawingData); // Force immediate style update with current toolbar values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('handleDrawingFinished: Applying toolbar values - color:', color, 'opacity:', opacity); // Apply to the preview layer immediately if (this.previewLayer) { this.updateLayerStyle(this.previewLayer, color, opacity); } // Also update the polygon from drawing data if it exists if (drawingData.polygon) { this.updateLayerStyle(drawingData.polygon, color, opacity); } this.showStatus('Cut drawing completed. Fill in the properties and save.', 'success'); } handleDrawingCancelled() { console.log('handleDrawingCancelled called'); const toolbar = document.getElementById('cut-drawing-toolbar'); if (toolbar) { toolbar.classList.remove('active'); } // Clear stored drawing data this.currentDrawingData = null; const geojsonField = document.getElementById('cut-geojson'); const boundsField = document.getElementById('cut-bounds'); if (geojsonField) geojsonField.value = ''; if (boundsField) boundsField.value = ''; // Clear form dataset const form = document.getElementById('cut-form'); if (form) { delete form.dataset.geojson; delete form.dataset.bounds; } const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = true; } // Reset start drawing button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Start Drawing'; } console.log('Drawing cancelled, state reset'); this.showStatus('Cut drawing cancelled', 'info'); } reset() { console.log('Resetting cut form and drawing...'); // Reset the drawing if active if (this.cutDrawing) { this.cutDrawing.reset(); } // Reset form if (this.cutForm) { this.cutForm.reset(); // Set default values const colorInput = document.getElementById('cut-color'); const opacityInput = document.getElementById('cut-opacity'); if (colorInput) colorInput.value = '#3388ff'; if (opacityInput) opacityInput.value = '0.3'; this.updateColorValue(); this.updateOpacityValue(); } // Hide drawing toolbar const drawingToolbar = document.getElementById('cut-drawing-toolbar'); if (drawingToolbar) { drawingToolbar.classList.remove('active'); } // Reset buttons const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Start Drawing'; startDrawingBtn.classList.remove('danger'); } // Hide location management panel this.hideLocationManagement(); console.log('Cut form and drawing reset complete'); } async handleFormSubmit(event) { event.preventDefault(); console.log('Form submitted!'); const formData = new FormData(this.form); // Use this.form instead of this.cutForm console.log('Form data entries:'); for (let [key, value] of formData.entries()) { console.log(`${key}: ${value}`); } const cutData = { name: formData.get('name'), // Use the actual HTML name attributes description: formData.get('description'), color: this.getCurrentColor(), // Get from toolbar instead of form opacity: this.getCurrentOpacity(), // Get from toolbar instead of form category: formData.get('category'), is_public: formData.has('is_public'), is_official: formData.has('is_official') }; // Add the geojson and bounds from stored data if (this.currentDrawingData || event.target.dataset.geojson) { cutData.geojson = this.currentDrawingData?.geojson || event.target.dataset.geojson; cutData.bounds = this.currentDrawingData?.bounds || event.target.dataset.bounds; } else if (this.editingCutId) { // If editing and no new drawing, keep existing geometry const existingCut = this.allCuts.find(c => c.id === this.editingCutId); if (existingCut) { cutData.geojson = existingCut.geojson; cutData.bounds = existingCut.bounds; } } else { // Also try to get from hidden form fields as fallback cutData.geojson = formData.get('geojson'); cutData.bounds = formData.get('bounds'); } console.log('Cut data:', cutData); if (!cutData.name || !cutData.geojson) { this.showStatus('Name and geometry are required', 'error'); console.log('Validation failed - missing name or geojson'); return; } try { let result; if (this.editingCutId) { result = await this.updateCut(this.editingCutId, cutData); } else { result = await this.createCut(cutData); } if (result) { this.resetForm(); this.currentDrawingData = null; await this.loadCuts(); } } catch (error) { console.error('Error saving cut:', error); this.showStatus('Failed to save cut', 'error'); } } // Add a new method to update the drawing preview updateDrawingPreview(drawingData) { if (!drawingData || !drawingData.geojson) return; try { const geojson = JSON.parse(drawingData.geojson); // Remove any existing preview layer if (this.previewLayer) { this.cutsMap.removeLayer(this.previewLayer); this.previewLayer = null; } // Get current toolbar colors - this is the key fix const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('updateDrawingPreview: Using toolbar values - color:', color, 'opacity:', opacity); // Create the GeoJSON layer with a static style object (not a function) // This allows setStyle() to work properly later this.previewLayer = L.geoJSON(geojson, { style: { color: color, fillColor: color, fillOpacity: opacity, weight: 2, opacity: 0.8, // Change border opacity to 0.8 for consistency className: 'cut-polygon', dashArray: '3, 3' } }).addTo(this.cutsMap); // Add the cut-polygon CSS class to the path element and force inline styles if (this.previewLayer._layers) { Object.values(this.previewLayer._layers).forEach(layer => { if (layer._path) { layer._path.classList.add('cut-polygon'); // Force the fill-opacity inline style with important flag layer._path.style.setProperty('fill-opacity', opacity, 'important'); layer._path.style.setProperty('fill', color, 'important'); } }); } // Also check if we need to access sub-layers if (this.previewLayer._renderer && this.previewLayer._renderer._container) { const paths = this.previewLayer._renderer._container.querySelectorAll('path'); paths.forEach(path => { path.classList.add('cut-polygon'); // Force the fill-opacity inline style on all paths with important flag path.style.setProperty('fill-opacity', opacity, 'important'); path.style.setProperty('fill', color, 'important'); }); } // Force initial style application using our unified method this.updateLayerStyle(this.previewLayer, color, opacity); console.log('Drawing preview updated with opacity:', opacity); // Fit map to bounds if available if (drawingData.bounds) { const bounds = JSON.parse(drawingData.bounds); this.cutsMap.fitBounds(bounds, { padding: [20, 20] }); } } catch (error) { console.error('Error updating drawing preview:', error); } } // Method to refresh preview with current drawing data and form values refreshPreview() { if (this.currentDrawingData) { console.log('Refreshing preview with current form values'); this.updateDrawingPreview(this.currentDrawingData); } } async createCut(cutData) { try { const response = await fetch('/api/cuts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(cutData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to create cut: ${response.statusText}`); } const result = await response.json(); this.showStatus('Cut created successfully', 'success'); return result; } catch (error) { console.error('Error creating cut:', error); this.showStatus(error.message, 'error'); return null; } } async updateCut(id, cutData) { try { const response = await fetch(`/api/cuts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(cutData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to update cut: ${response.statusText}`); } const result = await response.json(); this.showStatus('Cut updated successfully', 'success'); return result; } catch (error) { console.error('Error updating cut:', error); this.showStatus(error.message, 'error'); return null; } } async loadCuts() { try { const response = await fetch('/api/cuts', { credentials: 'include' }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to load cuts:', response.status, errorText); throw new Error(`Failed to load cuts: ${response.statusText}`); } const data = await response.json(); // Handle case where cuts table might not exist or be empty if (!data.list) { console.log('No cuts data returned, initializing empty list'); data.list = []; } this.allCuts = data.list || []; this.filteredCuts = [...this.allCuts]; this.renderCutsList(); } catch (error) { console.error('Error loading cuts:', error); this.showNotification('Failed to load cuts. Please check if the cuts table exists.', 'error'); // Initialize with empty array so the UI still works this.allCuts = []; this.filteredCuts = []; this.renderCutsList(); } } renderCutsList() { if (!this.cutsList) return; // Remove existing event listener to prevent duplicates this.cutsList.removeEventListener('click', this.boundHandleCutActionClick); if (this.filteredCuts.length === 0) { this.cutsList.innerHTML = '

No cuts found

'; return; } const html = this.filteredCuts.map(cut => this.renderCutItem(cut)).join(''); this.cutsList.innerHTML = html; // Add event delegation for cut action buttons this.cutsList.addEventListener('click', this.boundHandleCutActionClick); } renderCutItem(cut) { console.log('Rendering cut item:', cut); // Debug log to see the cut structure const badges = []; const isPublic = cut.is_public || cut.Is_public || cut['Public Visibility']; const isOfficial = cut.is_official || cut.Is_official || cut['Official Cut']; if (isPublic) badges.push('Public'); else badges.push('Private'); if (isOfficial) badges.push('Official'); // Check different possible ID field names const cutId = cut.id || cut.Id || cut.ID || cut._id; const cutName = cut.name || cut.Name || 'Unknown'; const cutDescription = cut.description || cut.Description || ''; const cutCategory = cut.category || cut.Category || 'Custom'; const cutCreatedAt = cut.CreatedAt || cut.created_at || new Date().toISOString(); console.log('Cut ID found:', cutId, 'from cut object keys:', Object.keys(cut)); return `
${cutName} ${cutCategory}
${cutDescription ? `
${cutDescription}
` : ''}
${badges.join('')}
${new Date(cutCreatedAt).toLocaleDateString()}
`; } handleCutActionClick(event) { console.log('handleCutActionClick called', event); const button = event.target; console.log('Button:', button, 'Has data-action:', button.hasAttribute('data-action')); if (!button.hasAttribute('data-action')) return; event.preventDefault(); event.stopPropagation(); const action = button.getAttribute('data-action'); const cutId = button.getAttribute('data-cut-id'); console.log('Action:', action, 'Cut ID:', cutId); if (!cutId) return; switch (action) { case 'view': console.log('Calling viewCut'); this.viewCut(cutId); break; case 'edit': console.log('Calling editCut'); this.editCut(cutId); break; case 'duplicate': console.log('Calling duplicateCut'); this.duplicateCut(cutId); break; case 'delete': console.log('Calling deleteCut'); this.deleteCut(cutId); break; default: console.warn('Unknown cut action:', action); } } async viewCut(cutId) { console.log('viewCut called with ID:', cutId); const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId); console.log('Found cut:', cut); if (!cut) return; this.displayCut(cut); this.showLocationManagement(cutId); const cutName = cut.name || cut.Name || 'Unknown'; this.showStatus(`Viewing cut: ${cutName}`, 'info'); } displayCut(cutData) { if (this.currentCutLayer) { this.cutsMap.removeLayer(this.currentCutLayer); this.currentCutLayer = null; } if (!cutData) return false; // Get geojson from different possible field names const geojson = cutData.geojson || cutData.Geojson || cutData.GeoJSON || cutData['GeoJSON Data']; if (!geojson) { console.error('No geojson data found in cut:', cutData); return false; } try { const parsedGeojson = JSON.parse(geojson); // Get color and opacity from different possible field names const color = cutData.color || cutData.Color || '#3388ff'; const opacity = cutData.opacity || cutData.Opacity || 0.3; console.log('displayCut: Using color:', color, 'opacity:', opacity); // Create GeoJSON layer with static style object (not function) for proper setStyle() support this.currentCutLayer = L.geoJSON(parsedGeojson, { style: { color: color, fillColor: color, fillOpacity: opacity, weight: 2, opacity: 1.0 // Keep stroke solid } }); this.currentCutLayer.addTo(this.cutsMap); // Force apply the opacity using our enhanced styling approach this.updateLayerStyle(this.currentCutLayer, color, opacity); console.log('displayCut: Created currentCutLayer with opacity:', opacity); // Get bounds from different possible field names const bounds = cutData.bounds || cutData.Bounds; if (bounds) { try { const parsedBounds = JSON.parse(bounds); this.cutsMap.fitBounds(parsedBounds, { padding: [20, 20] }); } catch (boundsError) { this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] }); } } else { this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] }); } return true; } catch (error) { console.error('Error displaying cut:', error); this.showStatus('Failed to display cut', 'error'); return false; } } async editCut(cutId) { console.log('editCut called with ID:', cutId); const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId); console.log('Found cut for editing:', cut); if (!cut) return; this.editingCutId = cutId; // Use both lowercase and uppercase field names document.getElementById('cut-name').value = cut.name || cut.Name || ''; document.getElementById('cut-description').value = cut.description || cut.Description || ''; // Set toolbar values (these are the ones we actually use now) const cutColor = cut.color || cut.Color || '#3388ff'; const cutOpacity = cut.opacity || cut.Opacity || 0.3; const toolbarColor = document.getElementById('toolbar-color'); const toolbarOpacity = document.getElementById('toolbar-opacity'); const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display'); if (toolbarColor) toolbarColor.value = cutColor; if (toolbarOpacity) toolbarOpacity.value = cutOpacity; if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = Math.round(cutOpacity * 100) + '%'; document.getElementById('cut-category').value = cut.category || cut.Category || 'Custom'; document.getElementById('cut-public').checked = cut.is_public || cut.Is_public || cut['Public Visibility'] || false; document.getElementById('cut-official').checked = cut.is_official || cut.Is_official || cut['Official Cut'] || false; document.getElementById('cut-geojson').value = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || ''; document.getElementById('cut-bounds').value = cut.bounds || cut.Bounds || ''; document.getElementById('cut-id').value = cut.id || cut.Id || cut.ID || cut._id; // Store the existing geometry in form dataset const form = document.getElementById('cut-form'); const geojsonData = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data']; const boundsData = cut.bounds || cut.Bounds; if (form && geojsonData) { form.dataset.geojson = geojsonData; form.dataset.bounds = boundsData || ''; } const cutName = cut.name || cut.Name || 'Unknown'; document.getElementById('cut-form-title').textContent = `Edit Cut: ${cutName}`; document.getElementById('save-cut-btn').textContent = 'Update Cut'; document.getElementById('save-cut-btn').disabled = false; document.getElementById('cancel-edit-btn').style.display = 'inline-block'; document.getElementById('start-drawing-btn').style.display = 'none'; this.updateColorValue(); this.updateOpacityValue(); this.displayCut(cut); this.showStatus(`Editing cut: ${cutName}`, 'info'); } async duplicateCut(cutId) { console.log('duplicateCut called with ID:', cutId); const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId); console.log('Found cut for duplication:', cut); if (!cut) return; // Use both lowercase and uppercase field names const cutName = cut.name || cut.Name || 'Unknown'; const cutDescription = cut.description || cut.Description || ''; const cutColor = cut.color || cut.Color || '#3388ff'; const cutOpacity = cut.opacity || cut.Opacity || 0.3; const cutCategory = cut.category || cut.Category || 'Custom'; const cutGeojson = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || ''; const cutBounds = cut.bounds || cut.Bounds || ''; const duplicateData = { name: `${cutName} (Copy)`, description: cutDescription, color: cutColor, opacity: cutOpacity, category: cutCategory, is_public: false, is_official: false, geojson: cutGeojson, bounds: cutBounds }; console.log('Duplicate data:', duplicateData); const result = await this.createCut(duplicateData); if (result) { await this.loadCuts(); this.showStatus(`Duplicated cut: ${cutName}`, 'success'); } } async deleteCut(cutId) { console.log('deleteCut called with ID:', cutId); const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId); console.log('Found cut for deletion:', cut); if (!cut) return; const cutName = cut.name || cut.Name || 'Unknown'; if (!confirm(`Are you sure you want to delete the cut "${cutName}"? This action cannot be undone.`)) { return; } try { const response = await fetch(`/api/cuts/${cutId}`, { method: 'DELETE', credentials: 'include' }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to delete cut: ${response.statusText}`); } this.showStatus('Cut deleted successfully', 'success'); if (this.currentCutLayer) { this.cutsMap.removeLayer(this.currentCutLayer); this.currentCutLayer = null; } await this.loadCuts(); } catch (error) { console.error('Error deleting cut:', error); this.showStatus(error.message, 'error'); } } cancelEdit() { this.resetForm(); // Hide the cancel button const cancelBtn = document.getElementById('cancel-edit-btn'); if (cancelBtn) { cancelBtn.style.display = 'none'; } } resetForm() { this.form.reset(); document.getElementById('cut-id').value = ''; document.getElementById('cut-geojson').value = ''; document.getElementById('cut-bounds').value = ''; // Reset toolbar values to defaults const toolbarColor = document.getElementById('toolbar-color'); const toolbarOpacity = document.getElementById('toolbar-opacity'); const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display'); if (toolbarColor) toolbarColor.value = '#3388ff'; if (toolbarOpacity) toolbarOpacity.value = '0.3'; if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = '30%'; // Update UI document.getElementById('cut-form-title').textContent = 'Cut Properties'; document.getElementById('cancel-edit-btn').style.display = 'none'; // Only disable save button (form inputs stay enabled) const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = true; } // Clear current cut this.currentCutId = null; // Clear any preview if (this.cutDrawing) { this.cutDrawing.clearPreview(); } } updateColorValue() { const colorInput = document.getElementById('cut-color'); const colorValue = document.getElementById('cut-color-text'); if (colorInput && colorValue) { colorValue.value = colorInput.value; } } updateOpacityValue() { const opacityInput = document.getElementById('cut-opacity'); const opacityValue = document.getElementById('opacity-value'); if (opacityInput && opacityValue) { const percentage = Math.round(opacityInput.value * 100); opacityValue.textContent = `${percentage}%`; } } filterCuts() { const searchTerm = document.getElementById('cuts-search').value.toLowerCase(); const categoryFilter = document.getElementById('cuts-category-filter').value; let filteredCuts = this.allCuts; if (searchTerm) { filteredCuts = filteredCuts.filter(cut => cut.name.toLowerCase().includes(searchTerm) || (cut.description && cut.description.toLowerCase().includes(searchTerm)) ); } if (categoryFilter) { filteredCuts = filteredCuts.filter(cut => cut.category === categoryFilter); } this.filteredCuts = filteredCuts; this.renderCutsList(); } exportCuts() { const exportData = { version: '1.0', timestamp: new Date().toISOString(), cuts: this.allCuts.map(cut => ({ name: cut.name, description: cut.description, color: cut.color, opacity: cut.opacity, category: cut.category, is_official: cut.is_official, geojson: cut.geojson, bounds: cut.bounds })) }; const data = JSON.stringify(exportData, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `cuts-export-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showStatus('Cuts exported successfully', 'success'); } async handleImportFile(event) { const file = event.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); if (!data.cuts || !Array.isArray(data.cuts)) { throw new Error('Invalid import file format'); } let successCount = 0; let errorCount = 0; for (const cutData of data.cuts) { const result = await this.createCut(cutData); if (result) { successCount++; } else { errorCount++; } } await this.loadCuts(); if (successCount > 0) { this.showStatus(`Successfully imported ${successCount} cuts${errorCount > 0 ? `, ${errorCount} failed` : ''}`, 'success'); } else { this.showStatus('No cuts were imported', 'error'); } } catch (error) { console.error('Import error:', error); this.showStatus('Failed to import cuts: ' + error.message, 'error'); } event.target.value = ''; } // Debug method to check form state debugFormState() { console.log('=== Form State Debug ==='); const inputs = [ 'cut-name', 'cut-description', 'cut-color', 'cut-opacity', 'cut-category', 'cut-public', 'cut-official', 'save-cut-btn' ]; inputs.forEach(id => { const element = document.getElementById(id); if (element) { console.log(`${id}: disabled=${element.disabled}, value="${element.value || element.checked}"`); } else { console.log(`${id}: NOT FOUND`); } }); console.log(`currentDrawingData exists: ${!!this.currentDrawingData}`); console.log(`previewLayer exists: ${!!this.previewLayer}`); console.log('========================'); } // Add a debug method to check layer opacity state specifically debugOpacityState() { const colorElement = document.getElementById('cut-color'); const opacityElement = document.getElementById('cut-opacity'); console.log('=== DEBUG: Opacity State ==='); console.log('Color value:', colorElement?.value); console.log('Opacity value:', opacityElement?.value); console.log('Opacity parsed:', parseFloat(opacityElement?.value)); if (this.previewLayer) { console.log('Preview layer exists'); this.previewLayer.eachLayer((layer) => { console.log('Layer options fillOpacity:', layer.options.fillOpacity); if (layer._path) { const svgOpacity = layer._path.getAttribute('fill-opacity'); const computedStyle = window.getComputedStyle(layer._path); console.log('SVG fill-opacity attribute:', svgOpacity); console.log('Computed fill-opacity style:', computedStyle.fillOpacity); console.log('SVG fill color:', layer._path.getAttribute('fill')); } }); } else { console.log('No preview layer found'); } if (this.currentCutLayer) { console.log('Current cut layer exists'); if (this.currentCutLayer.eachLayer) { this.currentCutLayer.eachLayer((layer) => { console.log('Current layer options fillOpacity:', layer.options.fillOpacity); if (layer._path) { console.log('Current SVG fill-opacity:', layer._path.getAttribute('fill-opacity')); } }); } } else { console.log('No current cut layer found'); } console.log('========================'); } showStatus(message, type) { // Use existing admin notification system if available if (typeof showNotification === 'function') { showNotification(message, type); } else { console.log(`[${type.toUpperCase()}] ${message}`); } } // Location Management Methods setupLocationManagementControls() { // 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()); } // Print cut view const printBtn = document.getElementById('print-cut-view'); if (printBtn) { printBtn.addEventListener('click', () => this.printCutView()); } // 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.cutsMap.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.cutsMap.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.cutsMap); } 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 printCutView() { if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } try { // Get cut information const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.currentCutId); const cutName = cut?.name || cut?.Name || 'Unknown Cut'; // Ensure locations are loaded before printing if (!this.currentCutLocations || this.currentCutLocations.length === 0) { this.showStatus('Loading locations for print...', 'info'); try { await this.loadCutLocations(); } catch (loadError) { console.log('Failed to load locations for print:', loadError); this.currentCutLocations = []; } } console.log('Print: Using', this.currentCutLocations.length, 'locations'); // Try multiple methods to capture the map let mapImageDataUrl = null; let mapInfo = ''; try { // Method 1: Try Leaflet's built-in screenshot capability using canvas mapImageDataUrl = await this.captureMapWithLeaflet(); if (!mapImageDataUrl) { // Method 2: Try using DOM-to-image if available mapImageDataUrl = await this.captureMapWithDomToImage(); } if (!mapImageDataUrl) { // Method 3: Try browser's built-in canvas capture mapImageDataUrl = await this.captureMapWithCanvas(); } if (mapImageDataUrl) { // We successfully captured the map mapInfo = `

Cut Map View

Cut Map

Map showing cut boundaries and ${this.currentCutLocations.length} location markers

`; } else { // Method 4: Generate a static map visualization const staticMap = await this.generateStaticMapImage(); if (staticMap) { mapInfo = staticMap; } else { // Fallback to map information throw new Error('All map capture methods failed'); } } } catch (mapError) { console.log('Map capture failed, using fallback info:', mapError); // Get map bounds and center for basic map info const mapBounds = this.cutsMap.getBounds(); const center = this.cutsMap.getCenter(); const zoom = this.cutsMap.getZoom(); mapInfo = `

Cut Boundaries & Location Summary

Map Center:
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}

Zoom Level: ${zoom}

Geographic Bounds:
North: ${mapBounds.getNorth().toFixed(6)}
South: ${mapBounds.getSouth().toFixed(6)}
East: ${mapBounds.getEast().toFixed(6)}
West: ${mapBounds.getWest().toFixed(6)}

Cut Statistics:
Total Locations: ${this.currentCutLocations.length}
Active Filters: ${this.getActiveFiltersCount()}
Showing: ${this.currentCutLocations.length} locations

Individual location coordinates and details are listed in the table below.

`; } // Create print content with map info and location data this.generatePrintContent(cutName, cut, mapInfo); } catch (error) { console.error('Error creating print view:', error); this.showStatus('Failed to create print view', 'error'); // Fallback to simple print without map this.printCutViewSimple(); } } async captureMapWithLeaflet() { try { // Simple approach: try to get the map's SVG or canvas content const mapContainer = this.cutsMap.getContainer(); // Look for the leaflet pane that contains the actual map content const mapPane = mapContainer.querySelector('.leaflet-map-pane'); if (!mapPane) { console.log('Leaflet map pane not found'); return null; } // Try to find any canvas elements in the map const canvases = mapPane.querySelectorAll('canvas'); if (canvases.length > 0) { console.log('Found canvas elements, attempting to capture...'); const canvas = canvases[0]; try { const dataUrl = canvas.toDataURL('image/png'); console.log('Successfully captured canvas'); return dataUrl; } catch (canvasError) { console.log('Canvas capture failed:', canvasError); } } // Try SVG capture const svgs = mapPane.querySelectorAll('svg'); if (svgs.length > 0) { console.log('Found SVG elements, attempting to capture...'); try { const svg = svgs[0]; const svgData = new XMLSerializer().serializeToString(svg); const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'}); const svgUrl = URL.createObjectURL(svgBlob); return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#f8f9fa'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(svgUrl); console.log('Successfully captured SVG'); resolve(canvas.toDataURL('image/png')); }; img.onerror = () => { URL.revokeObjectURL(svgUrl); console.log('SVG image load failed'); resolve(null); }; img.src = svgUrl; }); } catch (svgError) { console.log('SVG capture failed:', svgError); } } console.log('No capturable elements found in Leaflet map'); return null; } catch (error) { console.log('Leaflet capture failed:', error); return null; } } async captureMapWithDomToImage() { try { // Check if dom-to-image is available if (typeof domtoimage === 'undefined') { console.log('dom-to-image library not available'); return null; } const mapElement = this.cutsMap.getContainer(); // Add some options to improve capture quality const options = { width: 800, height: 600, quality: 0.95, bgcolor: '#f8f9fa', style: { transform: 'scale(1)', transformOrigin: 'top left', width: '800px', height: '600px' }, filter: (node) => { // Skip some problematic elements if (node.classList && node.classList.contains('leaflet-control-container')) { return false; } return true; } }; console.log('Attempting to capture map with dom-to-image...'); const dataUrl = await domtoimage.toPng(mapElement, options); console.log('Map capture successful with dom-to-image'); return dataUrl; } catch (error) { console.log('Dom-to-image capture failed:', error); return null; } } async captureMapWithCanvas() { try { // Try the simple canvas approach for SVG/Canvas elements const mapElement = this.cutsMap.getContainer(); // Check if the map container has a canvas child const canvasElements = mapElement.querySelectorAll('canvas'); if (canvasElements.length > 0) { // Use the first canvas const sourceCanvas = canvasElements[0]; return sourceCanvas.toDataURL('image/png'); } // Try to capture SVG elements const svgElements = mapElement.querySelectorAll('svg'); if (svgElements.length > 0) { const svg = svgElements[0]; const serializer = new XMLSerializer(); const svgData = serializer.serializeToString(svg); const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'}); const url = URL.createObjectURL(svgBlob); return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = svg.clientWidth || 800; canvas.height = svg.clientHeight || 600; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); resolve(canvas.toDataURL('image/png')); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(null); }; img.src = url; }); } return null; } catch (error) { console.log('Canvas capture failed:', error); return null; } } async generateStaticMapImage() { try { // Create a static map representation using CSS and HTML const mapBounds = this.cutsMap.getBounds(); const center = this.cutsMap.getCenter(); const zoom = this.cutsMap.getZoom(); // Get current locations for this cut const cutLocations = this.currentCutLocations || []; console.log('Static map: Using locations:', cutLocations.length); console.log('Static map: Sample location:', cutLocations[0]); // Calculate location positions within map bounds const locationMarkers = cutLocations.map(location => { const lat = parseFloat(location.latitude || location.Latitude || 0); const lng = parseFloat(location.longitude || location.Longitude || 0); console.log('Processing location:', location.first_name || location['First Name'], 'Coords:', lat, lng); if (!lat || !lng) { console.log('Invalid coordinates for location:', location); return null; } // Convert to percentage positions within the map bounds const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100; const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100; return { ...location, latPercent: Math.max(2, Math.min(98, 100 - latPercent)), // Invert Y for screen coordinates lngPercent: Math.max(2, Math.min(98, lngPercent)) }; }).filter(Boolean); console.log('Static map: Calculated positions for', locationMarkers.length, 'locations'); // Get the cut geometry for visualization let cutPath = ''; let cutPolygonPoints = ''; if (this.currentCutLayer && this.currentCutLayer.getLatLngs) { try { const latLngs = this.currentCutLayer.getLatLngs(); const coordinates = Array.isArray(latLngs[0]) ? latLngs[0] : latLngs; // Convert coordinates to percentages for the map visualization const pathPoints = coordinates.map(coord => { const lat = coord.lat; const lng = coord.lng; const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100; const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100; return `${Math.max(0, Math.min(100, lngPercent))}%,${Math.max(0, Math.min(100, 100 - latPercent))}%`; }); cutPolygonPoints = pathPoints.join(' '); if (pathPoints.length > 0) { cutPath = ` `; } } catch (cutError) { console.log('Could not extract cut geometry:', cutError); } } // Create a realistic map background pattern const mapBackground = ` background-image: /* Street grid pattern */ linear-gradient(0deg, rgba(200,200,200,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(200,200,200,0.3) 1px, transparent 1px), /* Neighborhood blocks */ linear-gradient(45deg, rgba(220,220,220,0.2) 25%, transparent 25%), linear-gradient(-45deg, rgba(220,220,220,0.2) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(220,220,220,0.2) 75%), linear-gradient(-45deg, transparent 75%, rgba(220,220,220,0.2) 75%); background-size: 40px 40px, 40px 40px, 80px 80px, 80px 80px, 80px 80px, 80px 80px; background-position: 0 0, 0 0, 0 0, 0 20px, 20px -20px, -20px 0px; background-color: #f0f8ff; `; // Create a simple visual map representation const mapVisualization = `
${cutPath} ${!cutPath ? '
' : ''} ${locationMarkers.map((location, index) => `
`).join('')}
N
Zoom: ${zoom}
Center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}
Support Levels
Strong Support
Lean Support
Oppose
Strong Oppose
Cut Boundary
`; return `

Cut Map View (${locationMarkers.length} locations)

${mapVisualization}

Visual representation of cut boundaries and location positions with realistic map styling

`; } catch (error) { console.log('Static map generation failed:', error); return null; } } 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 } } generatePrintContent(cutName, cut, mapContent) { const printWindow = window.open('', '_blank'); if (!printWindow) { this.showStatus('Popup blocked - please allow popups for print view', 'warning'); return; } const printContent = ` Cut: ${cutName}

Cut: ${cutName}

Generated: ${new Date().toLocaleString()}
Cut Category: ${cut?.category || cut?.Category || 'Unknown'}
Assigned To: ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
${this.getCurrentFiltersDisplay()}

Cut Statistics

${document.getElementById('total-locations')?.textContent || '0'}
Total Locations
${document.getElementById('support-1')?.textContent || '0'}
Strong Support
${document.getElementById('support-2')?.textContent || '0'}
Lean Support
${document.getElementById('has-signs')?.textContent || '0'}
Lawn Signs
${document.getElementById('has-email')?.textContent || '0'}
Email Contacts
${document.getElementById('has-phone')?.textContent || '0'}
Phone Contacts

Map Information

${mapContent}

Location Details

${this.currentCutLocations.map(location => { 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 || ''; const lat = location.latitude || location.Latitude || location.lat; const lng = location.longitude || location.Longitude || location.lng; const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A'; const supportLevel = location.support_level || location['Support Level'] || ''; const email = location.email || location.Email || ''; const phone = location.phone || location.Phone || ''; const contact = [email, phone].filter(Boolean).join(', '); const hasSign = location.sign || location.Sign ? 'Yes' : 'No'; const notes = location.notes || location.Notes || ''; return ` `; }).join('')}
Name Address Coordinates Support Level Contact Info Lawn Sign Notes
${name} ${address} ${coordinates} ${supportLevel} ${contact} ${hasSign} ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}

Generated by Campaign Map System - ${new Date().toLocaleDateString()}

`; try { printWindow.document.write(printContent); printWindow.document.close(); // Give the content time to render, then print setTimeout(() => { printWindow.print(); }, 500); this.showStatus('Print view generated successfully', 'success'); } catch (printError) { console.error('Error writing to print window:', printError); printWindow.close(); this.showStatus('Print window error - using fallback', 'warning'); this.printCutViewSimple(); } } printCutViewSimple() { // Simple print view without map image if (!this.currentCutId) { this.showStatus('No cut selected', 'warning'); return; } const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.currentCutId); const cutName = cut?.name || cut?.Name || 'Unknown Cut'; const printWindow = window.open('', '_blank'); if (!printWindow) { this.showStatus('Popup blocked - please allow popups for print view', 'warning'); return; } const printContent = ` Cut: ${cutName}

Cut: ${cutName}

Generated: ${new Date().toLocaleString()}
Cut Category: ${cut?.category || cut?.Category || 'Unknown'}
Assigned To: ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
${this.getCurrentFiltersDisplay()}

Cut Statistics

${document.getElementById('total-locations')?.textContent || '0'}
Total Locations
${document.getElementById('support-1')?.textContent || '0'}
Strong Support
${document.getElementById('support-2')?.textContent || '0'}
Lean Support
${document.getElementById('has-signs')?.textContent || '0'}
Lawn Signs
${document.getElementById('has-email')?.textContent || '0'}
Email Contacts
${document.getElementById('has-phone')?.textContent || '0'}
Phone Contacts

Location Details

${this.currentCutLocations.map(location => { 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 || ''; const lat = location.latitude || location.Latitude || location.lat; const lng = location.longitude || location.Longitude || location.lng; const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A'; const supportLevel = location.support_level || location['Support Level'] || ''; const email = location.email || location.Email || ''; const phone = location.phone || location.Phone || ''; const contact = [email, phone].filter(Boolean).join(', '); const hasSign = location.sign || location.Sign ? 'Yes' : 'No'; const notes = location.notes || location.Notes || ''; return ` `; }).join('')}
Name Address Coordinates Support Level Contact Info Lawn Sign Notes
${name} ${address} ${coordinates} ${supportLevel} ${contact} ${hasSign} ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}

Generated by Campaign Map System - ${new Date().toLocaleDateString()}

`; try { if (!printWindow || printWindow.closed) { this.showStatus('Print window unavailable', 'error'); return; } printWindow.document.write(printContent); printWindow.document.close(); // Give content time to load then print setTimeout(() => { if (!printWindow.closed) { printWindow.print(); } }, 300); this.showStatus('Simple print view generated', 'success'); } catch (printError) { console.error('Error in simple print:', printError); if (printWindow && !printWindow.closed) { printWindow.close(); } this.showStatus('Print failed - please try again', 'error'); } } 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; } 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.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId ); console.log('Found cut for location management:', cut); console.log('Available cuts:', this.allCuts.map(c => ({ id: c.id || c.Id || c.ID || c._id, name: c.name || c.Name }))); 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.cutsMap.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'); } } } // Global instance const adminCutsManager = new AdminCutsManager(); window.adminCutsManager = adminCutsManager; // Expose debug function globally for easy access window.debugCutsForm = () => adminCutsManager.debugFormState(); window.debugOpacity = () => adminCutsManager.debugOpacityState(); window.forceUpdateCutStyle = () => adminCutsManager.forceUpdateDrawingStyle(); window.syncToolbarValues = () => adminCutsManager.syncToolbarDisplayValues(); // Initialize when cuts section becomes visible document.addEventListener('DOMContentLoaded', function() { // Initialize immediately if cuts section is already visible const cutsSection = document.getElementById('cuts'); if (cutsSection && cutsSection.style.display !== 'none') { adminCutsManager.initialize().catch(error => { console.error('Failed to initialize cuts manager:', error); }); return; } // Otherwise, watch for it to become visible const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'style' && mutation.target.id === 'cuts' && mutation.target.style.display !== 'none' && !adminCutsManager.isInitialized) { // Small delay to ensure DOM is fully rendered setTimeout(() => { adminCutsManager.initialize().catch(error => { console.error('Failed to initialize cuts manager:', error); }); }, 100); } }); }); if (cutsSection) { observer.observe(cutsSection, { attributes: true }); } });