From 459cea0c3bdce206507cfc8aa7d7a55b9adc83b7 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 7 Sep 2025 11:50:44 -0600 Subject: [PATCH] Fixed the print view and things seem to be working now. --- map/app/public/admin.html | 6 +- map/app/public/css/admin.css | 1 + map/app/public/js/admin-core.js | 6 +- map/app/public/js/admin-cuts-main.js | 110 + map/app/public/js/admin-cuts-manager.js | 1575 ++++++++++ map/app/public/js/admin-cuts.js | 3303 +-------------------- map/app/public/js/cut-drawing-new.js | 425 +++ map/app/public/js/cut-drawing.js | 445 +-- map/app/public/js/cut-location-manager.js | 443 +++ map/app/public/js/cut-print-utils.js | 1052 +++++++ map/test_print_debug.html | 89 + 11 files changed, 4055 insertions(+), 3400 deletions(-) create mode 100644 map/app/public/js/admin-cuts-main.js create mode 100644 map/app/public/js/admin-cuts-manager.js create mode 100644 map/app/public/js/cut-drawing-new.js create mode 100644 map/app/public/js/cut-location-manager.js create mode 100644 map/app/public/js/cut-print-utils.js create mode 100644 map/test_print_debug.html diff --git a/map/app/public/admin.html b/map/app/public/admin.html index c666e45..23cb199 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -1377,7 +1377,11 @@ - + + + + + diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css index d1f17d5..0fb079c 100644 --- a/map/app/public/css/admin.css +++ b/map/app/public/css/admin.css @@ -16,3 +16,4 @@ /* Legacy import compatibility */ @import url("modules/dashboard.css"); +@import url("modules/cuts.css"); diff --git a/map/app/public/js/admin-core.js b/map/app/public/js/admin-core.js index a9ff84d..63b065b 100644 --- a/map/app/public/js/admin-core.js +++ b/map/app/public/js/admin-core.js @@ -209,7 +209,11 @@ function loadSectionData(sectionId) { console.error('Failed to initialize cuts manager:', error); }); } else { - console.log('Admin cuts manager already initialized'); + console.log('Admin cuts manager already initialized, refreshing map...'); + // Refresh map size when section becomes visible + if (window.adminCutsManager.refreshMapSize) { + window.adminCutsManager.refreshMapSize(); + } } } else { console.error('adminCutsManager not found in showSection'); diff --git a/map/app/public/js/admin-cuts-main.js b/map/app/public/js/admin-cuts-main.js new file mode 100644 index 0000000..5803069 --- /dev/null +++ b/map/app/public/js/admin-cuts-main.js @@ -0,0 +1,110 @@ +/** + * Admin Cuts Management Module + * Main initialization file that imports and orchestrates all cut management modules + */ + +// Global admin cuts manager instance +let adminCutsManager = null; + +// Initialize the admin cuts system when DOM is ready +document.addEventListener('DOMContentLoaded', async function() { + console.log('DOM loaded, initializing admin cuts system...'); + + try { + // Wait for all module classes to be loaded + if (typeof AdminCutsManager === 'undefined') { + console.error('AdminCutsManager class not loaded'); + return; + } + + if (typeof CutDrawing === 'undefined') { + console.error('CutDrawing class not loaded'); + return; + } + + if (typeof CutLocationManager === 'undefined') { + console.error('CutLocationManager class not loaded'); + return; + } + + if (typeof CutPrintUtils === 'undefined') { + console.error('CutPrintUtils class not loaded'); + return; + } + + // Create manager instance + adminCutsManager = new AdminCutsManager(); + + // Initialize the system + await adminCutsManager.initialize(); + + console.log('Admin cuts system initialized successfully'); + + // Make manager globally accessible for debugging + window.adminCutsManager = adminCutsManager; + + } catch (error) { + console.error('Failed to initialize admin cuts system:', error); + + // Show error notification if available + if (typeof showNotification === 'function') { + showNotification('Failed to initialize cuts management system', 'error'); + } + } +}); + +// Global functions for backward compatibility +function startDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.handleStartDrawing(); + } +} + +function finishDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.cutDrawing.finishDrawing(); + } +} + +function cancelDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.cutDrawing.cancelDrawing(); + } +} + +function resetForm() { + if (adminCutsManager) { + adminCutsManager.resetForm(); + } +} + +function exportCuts() { + if (adminCutsManager) { + adminCutsManager.exportCuts(); + } +} + +function refreshCuts() { + if (adminCutsManager) { + adminCutsManager.loadCuts(); + } +} + +// Debug functions +function debugFormState() { + if (adminCutsManager) { + adminCutsManager.debugFormState(); + } +} + +function debugOpacityState() { + if (adminCutsManager) { + adminCutsManager.debugOpacityState(); + } +} + +function forceUpdateDrawingStyle() { + if (adminCutsManager) { + adminCutsManager.forceUpdateDrawingStyle(); + } +} diff --git a/map/app/public/js/admin-cuts-manager.js b/map/app/public/js/admin-cuts-manager.js new file mode 100644 index 0000000..9fa6e0d --- /dev/null +++ b/map/app/public/js/admin-cuts-manager.js @@ -0,0 +1,1575 @@ +/** + * Admin Cuts Manager Module + * Main class for managing cuts with form handling and UI interactions + */ + +class AdminCutsManager { + constructor() { + this.cutsMap = null; + this.cutDrawing = null; + this.locationManager = null; + this.printUtils = null; + this.currentCutId = null; + this.allCuts = []; + this.filteredCuts = []; + this.currentCutLayer = null; + this.isInitialized = false; + + // Drawing and preview properties + this.currentDrawingData = null; + this.previewLayer = null; + this.editingCutId = null; + + // 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'); + // If already initialized but map needs refresh (e.g., section just became visible) + this.refreshMapSize(); + return; + } + + console.log('Initializing admin cuts manager...'); + + // Check if cuts section is visible, if not defer map initialization + const cutsSection = document.getElementById('cuts'); + const isVisible = cutsSection && cutsSection.style.display !== 'none'; + + if (isVisible) { + // Initialize map first if section is visible + this.initializeMap(); + } else { + console.log('Cuts section not visible, deferring map initialization...'); + // Set up observer to initialize map when section becomes visible + this.setupVisibilityObserver(); + } + + // Initialize form first + this.initializeFormState(); + + // Initialize cuts list element + this.cutsList = document.getElementById('cuts-list'); + + // Initialize drawing (only if map exists) + if (this.cutsMap) { + this.initializeDrawing(); + } + + // Initialize location manager (only if map exists) + if (this.cutsMap) { + this.locationManager = new CutLocationManager(this.cutsMap, this); + + // Initialize print utilities + this.printUtils = new CutPrintUtils(this.cutsMap, this, this.locationManager); + } + + // 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)); + + // 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'); + } + + // 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.handleImportFile(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()); + } + + // Add drawing toolbar button handlers + const finishDrawingBtn = document.getElementById('finish-cut-btn'); + 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'); + if (cancelDrawingBtn) { + cancelDrawingBtn.addEventListener('click', () => { + if (this.cutDrawing) { + this.cutDrawing.cancelDrawing(); + } + }); + } + + // Print cut view + const printBtn = document.getElementById('print-cut-view'); + if (printBtn) { + printBtn.addEventListener('click', () => this.printUtils.printCutView()); + } + } + + // 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); + } + } + + updateDrawingUI() { + if (!this.cutDrawing) return; + + const state = this.cutDrawing.getState(); + const vertexCount = document.getElementById('vertex-count'); + const finishBtn = document.getElementById('finish-cut-btn'); + 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'); + } + + setupVisibilityObserver() { + // Watch for when the cuts section becomes visible + const cutsSection = document.getElementById('cuts'); + if (!cutsSection) return; + + console.log('Setting up visibility observer for cuts section...'); + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'style') { + const isVisible = cutsSection.style.display !== 'none'; + if (isVisible && !this.cutsMap) { + console.log('Cuts section became visible, initializing map...'); + this.initializeMap(); + + // Initialize remaining components after map is ready + setTimeout(() => { + this.initializeDrawing(); + this.locationManager = new CutLocationManager(this.cutsMap, this); + this.printUtils = new CutPrintUtils(this.cutsMap, this, this.locationManager); + }, 100); + + // Stop observing once initialized + observer.disconnect(); + } else if (isVisible && this.cutsMap) { + // Map exists but might need refresh + this.refreshMapSize(); + } + } + }); + }); + + observer.observe(cutsSection, { + attributes: true, + attributeFilter: ['style'] + }); + } + + refreshMapSize() { + if (this.cutsMap) { + console.log('Refreshing map size...'); + // Force Leaflet to recalculate map size + setTimeout(() => { + this.cutsMap.invalidateSize(); + this.cutsMap.invalidateSize(true); // Force refresh + console.log('Map size refreshed'); + }, 50); + } + } + + 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'); + } + + async handleFormSubmit(event) { + event.preventDefault(); + + console.log('Form submitted!'); + + const formData = new FormData(this.form); + console.log('Form data entries:'); + for (let [key, value] of formData.entries()) { + console.log(`${key}: ${value}`); + } + + const cutData = { + name: formData.get('name'), + description: formData.get('description'), + color: this.getCurrentColor(), + opacity: this.getCurrentOpacity(), + 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, + 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.showStatus('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); + 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.locationManager.showLocationManagement(cutId); + + // Check if this cut should automatically show locations + const shouldShowLocations = (cut.show_locations || cut.Show_locations || cut['Show Locations']) !== false; + console.log('Cut show_locations setting:', shouldShowLocations); + + if (shouldShowLocations) { + console.log('Auto-loading locations for cut view...'); + try { + await this.locationManager.loadCutLocations(); + // Update the toggle button to reflect that locations are shown + const toggleBtn = document.getElementById('toggle-location-visibility'); + if (toggleBtn) { + toggleBtn.textContent = 'Hide Locations'; + toggleBtn.classList.add('active'); + toggleBtn.classList.remove('inactive'); + } + } catch (error) { + console.log('Failed to auto-load locations:', error); + } + } + + 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.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(); + } + + // Hide location management panel + this.locationManager.hideLocationManagement(); + } + + 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}`); + } + } +} + +// Export the class if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = AdminCutsManager; +} else { + window.AdminCutsManager = AdminCutsManager; +} diff --git a/map/app/public/js/admin-cuts.js b/map/app/public/js/admin-cuts.js index 2664cc1..5803069 100644 --- a/map/app/public/js/admin-cuts.js +++ b/map/app/public/js/admin-cuts.js @@ -1,3247 +1,110 @@ /** * Admin Cuts Management Module - * Handles cut creation, editing, and management in the admin panel + * Main initialization file that imports and orchestrates all cut management modules */ -// 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; +// Global admin cuts manager instance +let adminCutsManager = null; - 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); - } - } - } +// Initialize the admin cuts system when DOM is ready +document.addEventListener('DOMContentLoaded', async function() { + console.log('DOM loaded, initializing admin cuts system...'); - 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'); + try { + // Wait for all module classes to be loaded + if (typeof AdminCutsManager === 'undefined') { + console.error('AdminCutsManager class not loaded'); 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'); + if (typeof CutDrawing === 'undefined') { + console.error('CutDrawing class not loaded'); return; } - // Check if map is already initialized - if (this.cutsMap) { - console.log('Cuts map already initialized'); + if (typeof CutLocationManager === 'undefined') { + console.error('CutLocationManager class not loaded'); 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...'); + if (typeof CutPrintUtils === 'undefined') { + console.error('CutPrintUtils class not loaded'); return; } - this.handleStartDrawing._processing = true; + // Create manager instance + adminCutsManager = new AdminCutsManager(); - 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); + // Initialize the system + await adminCutsManager.initialize(); - // Store the drawing data - this.currentDrawingData = drawingData; + console.log('Admin cuts system initialized successfully'); - // Hide drawing toolbar - document.getElementById('cut-drawing-toolbar').classList.remove('active'); + // Make manager globally accessible for debugging + window.adminCutsManager = adminCutsManager; - // Store geojson and bounds in hidden form fields - document.getElementById('cut-geojson').value = drawingData.geojson; - document.getElementById('cut-bounds').value = drawingData.bounds; + } catch (error) { + console.error('Failed to initialize admin cuts system:', error); - // 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 + // Show error notification if available if (typeof showNotification === 'function') { - showNotification(message, type); - } else { - console.log(`[${type.toUpperCase()}] ${message}`); + showNotification('Failed to initialize cuts management system', 'error'); } } - - // 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('')} - -
NameAddressCoordinatesSupport LevelContact InfoLawn SignNotes
${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('')} - -
NameAddressCoordinatesSupport LevelContact InfoLawn SignNotes
${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 }); - } }); + +// Global functions for backward compatibility +function startDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.handleStartDrawing(); + } +} + +function finishDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.cutDrawing.finishDrawing(); + } +} + +function cancelDrawing() { + if (adminCutsManager && adminCutsManager.cutDrawing) { + adminCutsManager.cutDrawing.cancelDrawing(); + } +} + +function resetForm() { + if (adminCutsManager) { + adminCutsManager.resetForm(); + } +} + +function exportCuts() { + if (adminCutsManager) { + adminCutsManager.exportCuts(); + } +} + +function refreshCuts() { + if (adminCutsManager) { + adminCutsManager.loadCuts(); + } +} + +// Debug functions +function debugFormState() { + if (adminCutsManager) { + adminCutsManager.debugFormState(); + } +} + +function debugOpacityState() { + if (adminCutsManager) { + adminCutsManager.debugOpacityState(); + } +} + +function forceUpdateDrawingStyle() { + if (adminCutsManager) { + adminCutsManager.forceUpdateDrawingStyle(); + } +} diff --git a/map/app/public/js/cut-drawing-new.js b/map/app/public/js/cut-drawing-new.js new file mode 100644 index 0000000..7758ff6 --- /dev/null +++ b/map/app/public/js/cut-drawing-new.js @@ -0,0 +1,425 @@ +/** + * Cut Drawing Module + * Handles polygon drawing functionality for cut creation + */ + +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(); + } + } +} + +// Export the class if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = CutDrawing; +} else { + window.CutDrawing = CutDrawing; +} diff --git a/map/app/public/js/cut-drawing.js b/map/app/public/js/cut-drawing.js index faddfbf..7758ff6 100644 --- a/map/app/public/js/cut-drawing.js +++ b/map/app/public/js/cut-drawing.js @@ -1,22 +1,23 @@ /** * Cut Drawing Module - * Handles polygon drawing functionality for creating map cuts + * Handles polygon drawing functionality for cut creation */ -export class CutDrawing { - constructor(map, options = {}) { +class CutDrawing { + constructor(map) { this.map = map; this.vertices = []; this.markers = []; this.polyline = null; - this.previewPolygon = null; // Add preview polygon + this.polygon = null; // Add polygon preview + this.finalPolygon = null; // Final polygon after finishing this.isDrawing = false; - this.onComplete = options.onComplete || null; + this.onFinishCallback = null; + this.onCancelCallback = null; + this.currentColor = '#3388ff'; + this.currentOpacity = 0.3; } - /** - * Start drawing mode - */ startDrawing(onFinish, onCancel) { if (this.isDrawing) { this.cancelDrawing(); @@ -28,39 +29,64 @@ export class CutDrawing { this.vertices = []; this.markers = []; - // Change cursor and add click listener this.map.getContainer().style.cursor = 'crosshair'; this.map.on('click', this.onMapClick.bind(this)); - - // Disable double-click zoom while drawing this.map.doubleClickZoom.disable(); console.log('Cut drawing started - click to add points'); } - /** - * Handle map clicks to add vertices - */ onMapClick(e) { if (!this.isDrawing) return; - // Add vertex marker + // 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', - html: '
', + 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); - - // Update the polyline 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(); @@ -69,81 +95,126 @@ export class CutDrawing { console.log(`Added vertex ${this.vertices.length} at`, e.latlng); } - /** - * Update the polyline connecting vertices - */ updatePolyline() { - // Remove existing polyline + // Use stored color and opacity values + const color = this.currentColor; + const opacity = this.currentOpacity; + if (this.polyline) { this.map.removeLayer(this.polyline); } - - if (this.vertices.length > 1) { - // Create polyline connecting all vertices - this.polyline = L.polyline(this.vertices, { - color: '#3388ff', - weight: 2, - dashArray: '5, 5', - opacity: 0.8 - }).addTo(this.map); - } - } - - /** - * Finish drawing and create polygon - */ - finishDrawing() { - if (this.vertices.length < 3) { - alert('A cut must have at least 3 points'); - return; + + if (this.polygon) { + this.map.removeLayer(this.polygon); + this.polygon = null; } - // Create the polygon - const latlngs = this.vertices.map(v => v.getLatLng()); - - // Close the polygon - latlngs.push(latlngs[0]); - - // Generate GeoJSON - const geojson = { - type: 'Polygon', - coordinates: [latlngs.map(ll => [ll.lng, ll.lat])] - }; - - // Calculate bounds - const bounds = { - north: Math.max(...latlngs.map(ll => ll.lat)), - south: Math.min(...latlngs.map(ll => ll.lat)), - east: Math.max(...latlngs.map(ll => ll.lng)), - west: Math.min(...latlngs.map(ll => ll.lng)) - }; - - console.log('Cut drawing finished with', this.vertices.length, 'vertices'); - - // Show preview before clearing drawing - const color = document.getElementById('cut-color')?.value || '#3388ff'; - const opacity = parseFloat(document.getElementById('cut-opacity')?.value) || 0.3; - this.showPreview(geojson, color, opacity); - - // Clean up drawing elements - this.clearDrawing(); - - // Call completion callback with the data - if (this.onComplete && typeof this.onComplete === 'function') { - console.log('Calling completion callback with geojson and bounds'); - this.onComplete(geojson, bounds); - } else { - console.error('No completion callback defined'); + 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); + } } - - // Reset state - this.isDrawing = false; - this.updateToolbar(); } - /** - * Cancel drawing - */ + 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; @@ -153,22 +224,22 @@ export class CutDrawing { if (this.onCancelCallback) { this.onCancelCallback(); } + + // Also call the onCancel callback if set + if (this.onCancel) { + this.onCancel(); + } } - /** - * Remove the last added vertex - */ undoLastVertex() { if (!this.isDrawing || this.vertices.length === 0) return; - // Remove last vertex and marker this.vertices.pop(); const lastMarker = this.markers.pop(); if (lastMarker) { this.map.removeLayer(lastMarker); } - // Update polyline this.updatePolyline(); // Call update callback if available @@ -179,24 +250,23 @@ export class CutDrawing { console.log('Removed last vertex, remaining:', this.vertices.length); } - /** - * Clear all vertices and start over - */ clearVertices() { if (!this.isDrawing) return; - // Remove all markers this.markers.forEach(marker => { this.map.removeLayer(marker); }); - // Remove polyline if (this.polyline) { this.map.removeLayer(this.polyline); this.polyline = null; } - // Reset arrays + if (this.polygon) { + this.map.removeLayer(this.polygon); + this.polygon = null; + } + this.vertices = []; this.markers = []; @@ -208,41 +278,64 @@ export class CutDrawing { console.log('Cleared all vertices'); } - /** - * Cleanup drawing state - */ - cleanup() { - // Remove all markers + cleanupDrawingElements() { + // Remove drawing elements but preserve final polygon this.markers.forEach(marker => { this.map.removeLayer(marker); }); - // Remove polyline if (this.polyline) { this.map.removeLayer(this.polyline); } - // Reset cursor + if (this.polygon) { + this.map.removeLayer(this.polygon); + } + this.map.getContainer().style.cursor = ''; - - // Remove event listeners this.map.off('click', this.onMapClick); - - // Re-enable double-click zoom this.map.doubleClickZoom.enable(); - // Reset state 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; } - /** - * Get current drawing state - */ getState() { return { isDrawing: this.isDrawing, @@ -250,87 +343,83 @@ export class CutDrawing { canFinish: this.vertices.length >= 3 }; } - - /** - * Preview polygon without finishing - */ - showPreview(geojson, color = '#3388ff', opacity = 0.3) { - this.clearPreview(); + + // Add method to update current drawing style + updateDrawingStyle(color, opacity) { + this.currentColor = color; + this.currentOpacity = opacity; - if (!geojson) return; + console.log(`CutDrawing.updateDrawingStyle called with color: ${color}, opacity: ${opacity}`); - try { - const coordinates = geojson.coordinates[0]; - const latlngs = coordinates.map(coord => L.latLng(coord[1], coord[0])); - - this.previewPolygon = L.polygon(latlngs, { + // Update polyline color + if (this.polyline) { + this.polyline.setStyle({ color: color }); + } + + // Update polygon if it exists + if (this.polygon) { + this.polygon.setStyle({ color: color, - weight: 2, - opacity: 0.8, fillColor: color, fillOpacity: opacity, - className: 'cut-preview-polygon' - }).addTo(this.map); - - // Add CSS class for opacity control - const pathElement = this.previewPolygon.getElement(); - if (pathElement) { - pathElement.classList.add('cut-polygon'); - console.log('Added cut-polygon class to preview polygon'); - } - - console.log('Preview polygon shown with opacity:', opacity); - } catch (error) { - console.error('Error showing preview polygon:', error); - } - } - - /** - * Update preview polygon style without recreating it - */ - updatePreview(color = '#3388ff', opacity = 0.3) { - if (this.previewPolygon) { - this.previewPolygon.setStyle({ - color: color, - weight: 2, - opacity: 0.8, - fillColor: color, - fillOpacity: opacity + opacity: 0.8 // Border opacity }); - // Ensure CSS class is still present - const pathElement = this.previewPolygon.getElement(); - if (pathElement) { - pathElement.classList.add('cut-polygon'); + // 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; } - console.log('Preview polygon style updated with opacity:', opacity); + // 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() { - if (this.previewPolygon) { - this.map.removeLayer(this.previewPolygon); - this.previewPolygon = null; + // 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; } } - - /** - * Update drawing style (called from admin cuts manager) - */ - updateDrawingStyle(color = '#3388ff', opacity = 0.3) { - // Update the polyline connecting vertices if it exists - if (this.polyline) { - this.polyline.setStyle({ - color: color, - weight: 2, - opacity: 0.8 - }); + + // 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(); } - - // Update preview polygon if it exists - this.updatePreview(color, opacity); - - console.log('Cut drawing style updated with color:', color, 'opacity:', opacity); } } + +// Export the class if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = CutDrawing; +} else { + window.CutDrawing = CutDrawing; +} diff --git a/map/app/public/js/cut-location-manager.js b/map/app/public/js/cut-location-manager.js new file mode 100644 index 0000000..8172363 --- /dev/null +++ b/map/app/public/js/cut-location-manager.js @@ -0,0 +1,443 @@ +/** + * Cut Location Management Module + * Handles location display, filtering, statistics, and management for cuts + */ + +class CutLocationManager { + constructor(map, cutsManager) { + this.map = map; + this.cutsManager = cutsManager; + this.currentCutId = null; + this.currentCutLocations = []; + this.currentFilters = {}; + this.locationMarkersLayer = null; + this.showingLocations = false; + + this.setupEventHandlers(); + } + + setupEventHandlers() { + // Toggle location visibility + const toggleLocationBtn = document.getElementById('toggle-location-visibility'); + if (toggleLocationBtn) { + toggleLocationBtn.addEventListener('click', () => this.toggleLocationVisibility()); + } + + // Export cut locations + const exportBtn = document.getElementById('export-cut-locations'); + if (exportBtn) { + exportBtn.addEventListener('click', () => this.exportCutLocations()); + } + + // Apply filters + const applyFiltersBtn = document.getElementById('apply-filters'); + if (applyFiltersBtn) { + applyFiltersBtn.addEventListener('click', () => this.applyLocationFilters()); + } + + // Clear filters + const clearFiltersBtn = document.getElementById('clear-filters'); + if (clearFiltersBtn) { + clearFiltersBtn.addEventListener('click', () => this.clearLocationFilters()); + } + + // Save cut settings + const saveSettingsBtn = document.getElementById('save-cut-settings'); + if (saveSettingsBtn) { + saveSettingsBtn.addEventListener('click', () => this.saveCutSettings()); + } + } + + async toggleLocationVisibility() { + if (!this.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + const toggleBtn = document.getElementById('toggle-location-visibility'); + + if (this.showingLocations) { + // Hide locations + if (this.locationMarkersLayer) { + this.map.removeLayer(this.locationMarkersLayer); + } + this.showingLocations = false; + toggleBtn.textContent = 'Show Locations'; + toggleBtn.classList.remove('active'); + toggleBtn.classList.add('inactive'); + } else { + // Show locations + await this.loadCutLocations(); + toggleBtn.textContent = 'Hide Locations'; + toggleBtn.classList.add('active'); + toggleBtn.classList.remove('inactive'); + } + } + + async loadCutLocations() { + if (!this.currentCutId) return; + + try { + const filters = this.getCurrentFilters(); + const queryParams = new URLSearchParams(filters); + + const response = await fetch(`/api/cuts/${this.currentCutId}/locations?${queryParams}`, { + credentials: 'include' + }); + + if (!response.ok) { + throw new Error(`Failed to load locations: ${response.statusText}`); + } + + const data = await response.json(); + this.currentCutLocations = data.locations || []; + + // Update statistics + this.updateStatistics(data.statistics); + + // Display locations on map + this.displayLocationsOnMap(this.currentCutLocations); + + this.showingLocations = true; + this.showStatus(`Loaded ${this.currentCutLocations.length} locations`, 'success'); + + } catch (error) { + console.error('Error loading cut locations:', error); + this.showStatus('Failed to load locations', 'error'); + } + } + + displayLocationsOnMap(locations) { + // Remove existing markers + if (this.locationMarkersLayer) { + this.map.removeLayer(this.locationMarkersLayer); + } + + // Create new markers layer + this.locationMarkersLayer = L.layerGroup(); + + locations.forEach(location => { + if (location.latitude && location.longitude) { + const marker = L.marker([location.latitude, location.longitude], { + icon: this.createLocationIcon(location) + }); + + const popupContent = this.createLocationPopup(location); + marker.bindPopup(popupContent); + + this.locationMarkersLayer.addLayer(marker); + } + }); + + this.locationMarkersLayer.addTo(this.map); + } + + createLocationIcon(location) { + // Create different icons based on support level + const supportLevel = location.support_level || location['Support Level'] || 'unknown'; + const colors = { + '1': '#28a745', // Green - Strong support + '2': '#ffc107', // Yellow - Lean support + '3': '#fd7e14', // Orange - Lean opposition + '4': '#dc3545', // Red - Strong opposition + 'unknown': '#6c757d' // Grey - Unknown + }; + + const color = colors[supportLevel] || colors['unknown']; + + return L.divIcon({ + className: 'location-marker', + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8] + }); + } + + createLocationPopup(location) { + // Handle different possible field names for NocoDB + const firstName = location.first_name || location['First Name'] || ''; + const lastName = location.last_name || location['Last Name'] || ''; + const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown'; + + const address = location.address || location.Address || 'No address'; + const supportLevel = location.support_level || location['Support Level'] || 'Unknown'; + const hasSign = location.sign || location.Sign ? 'Yes' : 'No'; + const email = location.email || location.Email || ''; + const phone = location.phone || location.Phone || ''; + const notes = location.notes || location.Notes || ''; + + return ` +
+

${name}

+

Address: ${address}

+

Support Level: ${supportLevel}

+

Lawn Sign: ${hasSign}

+ ${email ? `

Email: ${email}

` : ''} + ${phone ? `

Phone: ${phone}

` : ''} + ${notes ? `

Notes: ${notes}

` : ''} +
+ `; + } + + getCurrentFilters() { + return { + support_level: document.getElementById('support-level-filter')?.value || '', + has_sign: document.getElementById('sign-status-filter')?.value || '', + sign_size: document.getElementById('sign-size-filter')?.value || '', + contact_filter: document.getElementById('contact-filter')?.value || '' + }; + } + + async applyLocationFilters() { + if (!this.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + await this.loadCutLocations(); + } + + clearLocationFilters() { + document.getElementById('support-level-filter').value = ''; + document.getElementById('sign-status-filter').value = ''; + document.getElementById('sign-size-filter').value = ''; + document.getElementById('contact-filter').value = ''; + + if (this.currentCutId && this.showingLocations) { + this.loadCutLocations(); + } + } + + updateStatistics(statistics) { + if (!statistics) return; + + document.getElementById('total-locations').textContent = statistics.total_locations || 0; + document.getElementById('support-1').textContent = statistics.support_levels?.['1'] || 0; + document.getElementById('support-2').textContent = statistics.support_levels?.['2'] || 0; + document.getElementById('has-signs').textContent = statistics.lawn_signs?.has_sign || 0; + document.getElementById('has-email').textContent = statistics.contact_info?.has_email || 0; + document.getElementById('has-phone').textContent = statistics.contact_info?.has_phone || 0; + + // Show statistics panel + document.getElementById('cut-statistics').style.display = 'block'; + } + + async exportCutLocations() { + if (!this.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + try { + const filters = this.getCurrentFilters(); + const queryParams = new URLSearchParams(filters); + + const response = await fetch(`/api/cuts/${this.currentCutId}/locations/export?${queryParams}`, { + credentials: 'include' + }); + + if (!response.ok) { + throw new Error(`Export failed: ${response.statusText}`); + } + + // Download the CSV file + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + + // Get filename from response header or use default + const contentDisposition = response.headers.get('content-disposition'); + const filename = contentDisposition + ? contentDisposition.split('filename=')[1]?.replace(/"/g, '') + : `cut_locations_${new Date().toISOString().split('T')[0]}.csv`; + + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + this.showStatus('Locations exported successfully', 'success'); + + } catch (error) { + console.error('Error exporting locations:', error); + this.showStatus('Failed to export locations', 'error'); + } + } + + async saveCutSettings() { + if (!this.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + try { + const settings = { + show_locations: document.getElementById('show-locations-toggle').checked, + export_enabled: document.getElementById('export-enabled-toggle').checked, + assigned_to: document.getElementById('assigned-to').value, + completion_percentage: parseInt(document.getElementById('completion-percentage').value) || 0 + }; + + const response = await fetch(`/api/cuts/${this.currentCutId}/settings`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(settings) + }); + + if (!response.ok) { + throw new Error(`Failed to save settings: ${response.statusText}`); + } + + this.showStatus('Cut settings saved successfully', 'success'); + + } catch (error) { + console.error('Error saving cut settings:', error); + this.showStatus('Failed to save cut settings', 'error'); + } + } + + showLocationManagement(cutId) { + console.log('showLocationManagement called with cutId:', cutId); + this.currentCutId = cutId; + const locationSection = document.getElementById('cut-location-management'); + console.log('Location section element:', locationSection); + + if (locationSection) { + console.log('Setting location section display to block'); + locationSection.style.display = 'block'; + + // Load cut data to populate settings - use multiple possible ID field names for NocoDB + const cut = this.cutsManager.allCuts.find(c => + (c.id || c.Id || c.ID || c._id) == cutId + ); + console.log('Found cut for location management:', cut); + + if (cut) { + const toggleElement = document.getElementById('show-locations-toggle'); + const exportElement = document.getElementById('export-enabled-toggle'); + const assignedElement = document.getElementById('assigned-to'); + const completionElement = document.getElementById('completion-percentage'); + + console.log('Setting up toggles:', { + toggleElement: !!toggleElement, + exportElement: !!exportElement, + assignedElement: !!assignedElement, + completionElement: !!completionElement + }); + + if (toggleElement) toggleElement.checked = (cut.show_locations || cut.Show_locations || cut['Show Locations']) !== false; + if (exportElement) exportElement.checked = (cut.export_enabled || cut.Export_enabled || cut['Export Enabled']) !== false; + if (assignedElement) assignedElement.value = cut.assigned_to || cut.Assigned_to || cut['Assigned To'] || ''; + if (completionElement) completionElement.value = cut.completion_percentage || cut.Completion_percentage || cut['Completion Percentage'] || ''; + } + } else { + console.error('Location management section not found!'); + } + } + + hideLocationManagement() { + const locationSection = document.getElementById('cut-location-management'); + if (locationSection) { + locationSection.style.display = 'none'; + } + + // Clear current data + this.currentCutId = null; + this.currentCutLocations = []; + + // Hide locations on map + if (this.locationMarkersLayer) { + this.map.removeLayer(this.locationMarkersLayer); + } + this.showingLocations = false; + + // Reset button state + const toggleBtn = document.getElementById('toggle-location-visibility'); + if (toggleBtn) { + toggleBtn.textContent = 'Show Locations'; + toggleBtn.classList.remove('active'); + toggleBtn.classList.remove('inactive'); + } + } + + showStatus(message, type) { + // Use existing admin notification system if available + if (typeof showNotification === 'function') { + showNotification(message, type); + } else if (this.cutsManager && this.cutsManager.showStatus) { + this.cutsManager.showStatus(message, type); + } else { + console.log(`[${type.toUpperCase()}] ${message}`); + } + } + + getSupportColor(supportLevel) { + switch(supportLevel) { + case '1': return '#28a745'; // Strong Support - Green + case '2': return '#ffc107'; // Lean Support - Yellow + case '3': return '#fd7e14'; // Oppose - Orange + case '4': return '#dc3545'; // Strong Oppose - Red + default: return '#6c757d'; // Unknown - Gray + } + } + + getCurrentFiltersDisplay() { + const activeFilters = []; + + // Check location filters + const supportFilter = document.getElementById('support-filter')?.value; + if (supportFilter && supportFilter !== 'all') { + activeFilters.push(`Support Level: ${supportFilter}`); + } + + const signFilter = document.getElementById('sign-filter')?.value; + if (signFilter && signFilter !== 'all') { + activeFilters.push(`Lawn Signs: ${signFilter === 'true' ? 'Yes' : 'No'}`); + } + + const contactFilter = document.getElementById('contact-filter')?.value; + if (contactFilter && contactFilter !== 'all') { + const contactLabels = { + 'email': 'Has Email', + 'phone': 'Has Phone', + 'both': 'Has Both Email & Phone', + 'none': 'No Contact Info' + }; + activeFilters.push(`Contact: ${contactLabels[contactFilter] || contactFilter}`); + } + + if (activeFilters.length === 0) { + return '
Filters: None (showing all locations)
'; + } + + return `
Active Filters: ${activeFilters.join(', ')}
`; + } + + getActiveFiltersCount() { + let count = 0; + + const supportFilter = document.getElementById('support-filter')?.value; + if (supportFilter && supportFilter !== 'all') count++; + + const signFilter = document.getElementById('sign-filter')?.value; + if (signFilter && signFilter !== 'all') count++; + + const contactFilter = document.getElementById('contact-filter')?.value; + if (contactFilter && contactFilter !== 'all') count++; + + return count; + } +} + +// Export the class if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = CutLocationManager; +} else { + window.CutLocationManager = CutLocationManager; +} diff --git a/map/app/public/js/cut-print-utils.js b/map/app/public/js/cut-print-utils.js new file mode 100644 index 0000000..5fb79d4 --- /dev/null +++ b/map/app/public/js/cut-print-utils.js @@ -0,0 +1,1052 @@ +/** + * Cut Print Utilities Module + * Handles map capture, print generation, and export functionality for cuts + */ + +class CutPrintUtils { + constructor(map, cutsManager, locationManager) { + this.map = map; + this.cutsManager = cutsManager; + this.locationManager = locationManager; + } + + async printCutView() { + if (!this.locationManager.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + try { + // Get cut information + const cut = this.cutsManager.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.locationManager.currentCutId); + const cutName = cut?.name || cut?.Name || 'Unknown Cut'; + + // Ensure locations are loaded AND visible on the map before printing + this.showStatus('Preparing map for print (loading locations)...', 'info'); + + // Force load locations if not already loaded + if (!this.locationManager.currentCutLocations || this.locationManager.currentCutLocations.length === 0) { + try { + await this.locationManager.loadCutLocations(); + } catch (loadError) { + console.log('Failed to load locations for print:', loadError); + this.locationManager.currentCutLocations = []; + } + } + + // Ensure locations are actually displayed on the map + if (this.locationManager.currentCutLocations.length > 0 && !this.locationManager.showingLocations) { + console.log('Print: Displaying locations on map for capture...'); + this.locationManager.displayLocationsOnMap(this.locationManager.currentCutLocations); + this.locationManager.showingLocations = true; + + // Update the UI toggle button to reflect the new state + const toggleBtn = document.getElementById('toggle-location-visibility'); + if (toggleBtn) { + toggleBtn.textContent = 'Hide Locations'; + toggleBtn.classList.add('active'); + toggleBtn.classList.remove('inactive'); + } + + // Give the map a moment to render the new markers + await new Promise(resolve => setTimeout(resolve, 500)); + } + + console.log('Print: Using', this.locationManager.currentCutLocations.length, 'locations'); + console.log('Print: Locations showing on map:', this.locationManager.showingLocations); + + // Debug: Check what's actually visible on the map + this.debugMapState(); + + // Try multiple methods to capture the map + let mapImageDataUrl = null; + let mapInfo = ''; + + try { + // Method 1: Try dom-to-image first (better for SVG coordinate preservation) + if (typeof domtoimage !== 'undefined') { + console.log('Attempting map capture with dom-to-image...'); + mapImageDataUrl = await this.captureMapWithDomToImage(); + if (mapImageDataUrl) { + console.log('✓ Successfully captured with dom-to-image'); + } + } + + if (!mapImageDataUrl) { + // Method 2: Try html2canvas if available (fallback for complex DOM) + if (typeof html2canvas !== 'undefined') { + console.log('Attempting map capture with html2canvas...'); + mapImageDataUrl = await this.captureMapWithHtml2Canvas(); + if (mapImageDataUrl) { + console.log('✓ Successfully captured with html2canvas'); + } + } + } + + if (!mapImageDataUrl) { + // Method 3: Try Leaflet's built-in screenshot capability using canvas + console.log('Attempting map capture with Leaflet canvas...'); + mapImageDataUrl = await this.captureMapWithLeaflet(); + if (mapImageDataUrl) { + console.log('✓ Successfully captured with Leaflet canvas'); + } + } + + if (!mapImageDataUrl) { + // Method 4: Try browser's built-in canvas capture + console.log('Attempting map capture with browser canvas...'); + mapImageDataUrl = await this.captureMapWithCanvas(); + if (mapImageDataUrl) { + console.log('✓ Successfully captured with browser canvas'); + } + } + + if (mapImageDataUrl) { + // We successfully captured the map + const locationCount = this.locationManager.currentCutLocations.length; + const showingLocations = this.locationManager.showingLocations; + console.log(`Successfully captured map with ${locationCount} locations (showing: ${showingLocations})`); + + mapInfo = ` +
+

Cut Map View

+ Cut Map +

+ Map showing cut boundaries and ${locationCount} location markers +

+
+ `; + } else { + // Method 5: Generate a static map visualization as fallback + console.log('All capture methods failed, generating static map...'); + const staticMap = await this.generateStaticMapImage(); + if (staticMap) { + console.log('✓ Generated static map fallback'); + mapInfo = staticMap; + } else { + // Final fallback to map information + console.log('Static map generation also failed'); + 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.map.getBounds(); + const center = this.map.getCenter(); + const zoom = this.map.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.locationManager.currentCutLocations.length}
+ Active Filters: ${this.locationManager.getActiveFiltersCount()}
+ Showing: ${this.locationManager.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.map.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 captureMapWithHtml2Canvas() { + try { + // Check if html2canvas is available + if (typeof html2canvas === 'undefined') { + console.log('html2canvas library not available'); + return null; + } + + const mapElement = this.map.getContainer(); + + console.log('Attempting to capture map with html2canvas...'); + + // Wait for any pending map animations/renders to complete + await new Promise(resolve => setTimeout(resolve, 200)); + + // Configure html2canvas options for better map capture + const options = { + allowTaint: false, + useCORS: true, + scale: 1, // Use scale 1 to avoid coordinate system shifts + width: mapElement.offsetWidth, + height: mapElement.offsetHeight, + backgroundColor: '#f8f9fa', + logging: false, + foreignObjectRendering: false, // Disable to avoid coordinate issues + removeContainer: true, + ignoreElements: (element) => { + // Skip zoom controls and attribution + if (element.classList) { + return element.classList.contains('leaflet-control-zoom') || + element.classList.contains('leaflet-control-attribution'); + } + return false; + } + }; + + const canvas = await html2canvas(mapElement, options); + const dataUrl = canvas.toDataURL('image/png', 0.95); // Slightly reduce quality for better performance + console.log('Map capture successful with html2canvas'); + return dataUrl; + + } catch (error) { + console.log('html2canvas 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.map.getContainer(); + + // Ensure the map container has proper dimensions + const containerRect = mapElement.getBoundingClientRect(); + const width = Math.max(800, containerRect.width); + const height = Math.max(600, containerRect.height); + + console.log(`Capturing map with dimensions: ${width}x${height}`); + + // Add some options to improve capture quality + const options = { + width: width, + height: height, + quality: 0.95, + bgcolor: '#f8f9fa', + style: { + transform: 'scale(1)', + transformOrigin: 'top left', + width: `${width}px`, + height: `${height}px` + }, + filter: (node) => { + // Skip attribution and some problematic elements but keep markers and overlays + if (node.classList) { + // Skip zoom controls and attribution + if (node.classList.contains('leaflet-control-zoom') || + node.classList.contains('leaflet-control-attribution')) { + return false; + } + // Keep marker layers, overlays, and the main map content + if (node.classList.contains('leaflet-marker-icon') || + node.classList.contains('leaflet-marker-shadow') || + node.classList.contains('leaflet-overlay-pane') || + node.classList.contains('leaflet-map-pane') || + node.classList.contains('leaflet-tile-pane') || + node.classList.contains('location-marker')) { + return true; + } + } + return true; + }, + // Use pixelRatio for better quality on high-DPI displays + pixelRatio: window.devicePixelRatio || 1 + }; + + console.log('Attempting to capture map with dom-to-image...'); + + // Try to capture the map element + 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.map.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.map.getBounds(); + const center = this.map.getCenter(); + const zoom = this.map.getZoom(); + + // Get current locations for this cut + const cutLocations = this.locationManager.currentCutLocations || []; + console.log('Static map: Using locations:', cutLocations.length); + + // 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.cutsManager.currentCutLayer && this.cutsManager.currentCutLayer.getLatLngs) { + try { + const latLngs = this.cutsManager.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; + + // Apply same clamping as location markers for consistency + const clampedLatPercent = Math.max(0, Math.min(100, 100 - latPercent)); // Invert Y for screen coordinates + const clampedLngPercent = Math.max(0, Math.min(100, lngPercent)); + + return `${clampedLngPercent}%,${clampedLatPercent}%`; + }); + + 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; + } + } + + 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.locationManager.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.locationManager.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('')} + +
NameAddressCoordinatesSupport LevelContact InfoLawn SignNotes
${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.locationManager.currentCutId) { + this.showStatus('No cut selected', 'warning'); + return; + } + + const cut = this.cutsManager.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.locationManager.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.locationManager.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.locationManager.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('')} + +
NameAddressCoordinatesSupport LevelContact InfoLawn SignNotes
${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'); + } + } + + 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}`); + } + } + + debugMapState() { + console.log('=== MAP STATE DEBUG ==='); + console.log('Map container:', this.map.getContainer()); + console.log('Map layers:', this.map._layers); + console.log('Current cut layer:', this.cutsManager.currentCutLayer); + console.log('Location markers layer:', this.locationManager.locationMarkersLayer); + console.log('Showing locations:', this.locationManager.showingLocations); + + // Check for visible markers + const mapContainer = this.map.getContainer(); + const markers = mapContainer.querySelectorAll('.leaflet-marker-icon, .location-marker'); + console.log('Visible marker elements:', markers.length); + + // Check for overlays + const overlays = mapContainer.querySelectorAll('.leaflet-overlay-pane *'); + console.log('Overlay elements:', overlays.length); + + // Debug cut layer positioning + if (this.cutsManager.currentCutLayer) { + try { + const bounds = this.cutsManager.currentCutLayer.getBounds(); + const mapBounds = this.map.getBounds(); + console.log('Cut layer bounds:', { + north: bounds.getNorth(), + south: bounds.getSouth(), + east: bounds.getEast(), + west: bounds.getWest() + }); + console.log('Map bounds:', { + north: mapBounds.getNorth(), + south: mapBounds.getSouth(), + east: mapBounds.getEast(), + west: mapBounds.getWest() + }); + + // Check the actual SVG path element + const pathElement = mapContainer.querySelector('path.cut-polygon'); + if (pathElement) { + console.log('Cut polygon SVG element found'); + console.log('Cut polygon d attribute length:', pathElement.getAttribute('d')?.length || 0); + console.log('Cut polygon transform:', pathElement.getAttribute('transform')); + console.log('Cut polygon fill:', pathElement.getAttribute('fill')); + console.log('Cut polygon fill-opacity:', pathElement.getAttribute('fill-opacity')); + + // Get the bounding box of the SVG element + const bbox = pathElement.getBBox(); + console.log('SVG path bounding box:', bbox); + } else { + console.log('No cut polygon SVG element found with class "cut-polygon"'); + // Look for any path elements + const allPaths = mapContainer.querySelectorAll('path'); + console.log('Total path elements found:', allPaths.length); + allPaths.forEach((path, index) => { + console.log(`Path ${index} classes:`, path.className.baseVal || path.className); + }); + } + } catch (error) { + console.log('Error getting cut layer debug info:', error); + } + } else { + console.log('No current cut layer found'); + } + + console.log('=== END MAP STATE DEBUG ==='); + } +} + +// Export the class if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = CutPrintUtils; +} else { + window.CutPrintUtils = CutPrintUtils; +} diff --git a/map/test_print_debug.html b/map/test_print_debug.html new file mode 100644 index 0000000..15a6361 --- /dev/null +++ b/map/test_print_debug.html @@ -0,0 +1,89 @@ + + + + Print Debug Test + + + +

Print Debug Test

+

Check the browser console for test results.

+

This tests the enhanced CutPrintUtils functionality.

+ +

Key Improvements Made:

+ + +

Root Cause Analysis:

+

The issue was that locations were not automatically displayed on the map when viewing a cut or printing. + The print function expected locations to be visible but they were only shown when the user manually clicked "Show Locations".

+ +

Solution:

+
    +
  1. Print Enhancement: The print function now ensures locations are loaded and displayed before capturing the map
  2. +
  3. View Enhancement: When viewing a cut, locations are automatically loaded if the cut has show_locations enabled
  4. +
  5. Capture Enhancement: Improved map capture methods with html2canvas as primary method
  6. +
  7. State Management: Better synchronization between location visibility and UI state
  8. +
+ +