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 `
+
+
+ ${cutDescription ? `
${cutDescription}
` : ''}
+
+
+ View
+ Edit
+ Duplicate
+ Delete
+
+
+ `;
+ }
+
+ 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 `
-
-
- ${cutDescription ? `
${cutDescription}
` : ''}
-
-
- View
- Edit
- Duplicate
- Delete
-
-
- `;
- }
-
- 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 `
-
- `;
- }
-
- 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
-
-
- 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('')}
-
-
-
-
-
-
-
-
-
-
-
- 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}
-
-
-
- ${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
-
-
-
- Name
- Address
- Coordinates
- Support Level
- Contact Info
- Lawn Sign
- Notes
-
-
-
- ${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 `
-
- ${name}
- ${address}
- ${coordinates}
- ${supportLevel}
- ${contact}
- ${hasSign}
- ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}
-
- `;
- }).join('')}
-
-
-
-
-
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}
-
-
-
- ${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
-
-
-
- Name
- Address
- Coordinates
- Support Level
- Contact Info
- Lawn Sign
- Notes
-
-
-
- ${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 `
-
- ${name}
- ${address}
- ${coordinates}
- ${supportLevel}
- ${contact}
- ${hasSign}
- ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}
-
- `;
- }).join('')}
-
-
-
-
-
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 `
+
+ `;
+ }
+
+ 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
+
+
+ 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('')}
+
+
+
+
+
+
+
+
+
+
+
+ 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}
+
+
+
+ ${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
+
+
+
+ Name
+ Address
+ Coordinates
+ Support Level
+ Contact Info
+ Lawn Sign
+ Notes
+
+
+
+ ${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 `
+
+ ${name}
+ ${address}
+ ${coordinates}
+ ${supportLevel}
+ ${contact}
+ ${hasSign}
+ ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}
+
+ `;
+ }).join('')}
+
+
+
+
+
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}
+
+
+
+ ${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
+
+
+
+ Name
+ Address
+ Coordinates
+ Support Level
+ Contact Info
+ Lawn Sign
+ Notes
+
+
+
+ ${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 `
+
+ ${name}
+ ${address}
+ ${coordinates}
+ ${supportLevel}
+ ${contact}
+ ${hasSign}
+ ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}
+
+ `;
+ }).join('')}
+
+
+
+
+
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:
+
+ ✅ Auto-load locations when printing if not already loaded
+ ✅ Auto-display locations on map for print capture
+ ✅ Enhanced map capture with html2canvas (priority #1)
+ ✅ Improved dom-to-image capture with better filtering
+ ✅ Better UI state management (toggle button updates)
+ ✅ Enhanced debugging and logging
+ ✅ Auto-show locations when viewing cuts (if enabled)
+
+
+ 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:
+
+ Print Enhancement: The print function now ensures locations are loaded and displayed before capturing the map
+ View Enhancement: When viewing a cut, locations are automatically loaded if the cut has show_locations enabled
+ Capture Enhancement: Improved map capture methods with html2canvas as primary method
+ State Management: Better synchronization between location visibility and UI state
+
+
+