freealberta/map/app/public/js/admin-cuts-manager.js
2025-09-10 19:20:03 -06:00

1905 lines
77 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
// Location markers for map display
this.locationMarkers = null;
// Pagination properties
this.currentPage = 1;
this.itemsPerPage = 5;
this.totalPages = 1;
// 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());
}
// Set up items per page selector
const itemsPerPageSelect = document.getElementById('cuts-per-page');
if (itemsPerPageSelect) {
itemsPerPageSelect.addEventListener('change', (e) => {
this.itemsPerPage = parseInt(e.target.value);
this.currentPage = 1; // Reset to first page
this.renderCutsList();
});
}
// 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 show locations toggle
const showLocationsToggle = document.getElementById('show-locations-on-map');
if (showLocationsToggle) {
showLocationsToggle.addEventListener('change', (e) => this.toggleLocationsOnMap(e.target.checked));
}
}
// 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];
console.log(`Loaded ${this.allCuts.length} cuts from server`);
if (this.allCuts.length >= 100) {
console.warn('Large dataset detected. Consider implementing pagination or server-side filtering.');
}
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 = '<p class="no-data">No cuts found</p>';
this.renderPagination(0);
return;
}
// Calculate pagination
this.totalPages = Math.ceil(this.filteredCuts.length / this.itemsPerPage);
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const cutsToShow = this.filteredCuts.slice(startIndex, endIndex);
// Render header with count and pagination info
const headerHtml = `
<div class="cuts-list-header">
<div class="cuts-count">
Showing ${startIndex + 1}-${Math.min(endIndex, this.filteredCuts.length)} of ${this.filteredCuts.length} cuts
${this.filteredCuts.length !== this.allCuts.length ? `(filtered from ${this.allCuts.length} total)` : ''}
</div>
</div>
`;
const cutsHtml = cutsToShow.map(cut => this.renderCutItem(cut)).join('');
this.cutsList.innerHTML = headerHtml + cutsHtml;
// Render pagination controls
this.renderPagination(this.filteredCuts.length);
// 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('<span class="cut-item-badge public">Public</span>');
else badges.push('<span class="cut-item-badge private">Private</span>');
if (isOfficial) badges.push('<span class="cut-item-badge official">Official</span>');
// 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 `
<div class="cut-item" data-cut-id="${cutId}">
<div class="cut-item-header">
<span class="cut-item-name">${cutName}</span>
<span class="cut-item-category ${cutCategory.toLowerCase()}">${cutCategory}</span>
</div>
${cutDescription ? `<div class="cut-item-description">${cutDescription}</div>` : ''}
<div class="cut-item-meta">
<div class="cut-item-badges">${badges.join('')}</div>
<span class="cut-item-date">${new Date(cutCreatedAt).toLocaleDateString()}</span>
</div>
<div class="cut-item-actions">
<button data-action="view" data-cut-id="${cutId}" class="primary">View</button>
<button data-action="edit" data-cut-id="${cutId}">Edit</button>
<button data-action="duplicate" data-cut-id="${cutId}">Duplicate</button>
<button data-action="delete" data-cut-id="${cutId}" class="danger">Delete</button>
</div>
</div>
`;
}
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;
// Reset location toggle to unchecked and hide locations
const showLocationsToggle = document.getElementById('show-locations-on-map');
if (showLocationsToggle) {
showLocationsToggle.checked = false;
this.toggleLocationsOnMap(false);
}
// 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 => {
// Handle different possible field names and null/undefined values
const cutName = cut.name || cut.Name || '';
const cutDescription = cut.description || cut.Description || '';
// Prioritize name matches - if name matches, return true immediately
if (cutName.toLowerCase().includes(searchTerm)) {
return true;
}
// Only check description if name doesn't match
return cutDescription.toLowerCase().includes(searchTerm);
});
// Sort results to prioritize name matches at the top
filteredCuts.sort((a, b) => {
const nameA = a.name || a.Name || '';
const nameB = b.name || b.Name || '';
const nameAMatches = nameA.toLowerCase().includes(searchTerm);
const nameBMatches = nameB.toLowerCase().includes(searchTerm);
// If both match by name or both don't match by name, maintain original order
if (nameAMatches === nameBMatches) {
return 0;
}
// Prioritize name matches (true comes before false)
return nameBMatches - nameAMatches;
});
}
if (categoryFilter) {
filteredCuts = filteredCuts.filter(cut => {
const cutCategory = cut.category || cut.Category || '';
return cutCategory === categoryFilter;
});
}
this.filteredCuts = filteredCuts;
// Reset to first page when filtering
this.currentPage = 1;
this.renderCutsList();
}
renderPagination(totalItems) {
const paginationContainer = document.getElementById('cuts-pagination') || this.createPaginationContainer();
if (totalItems <= this.itemsPerPage) {
paginationContainer.innerHTML = '';
paginationContainer.style.display = 'none';
return;
}
paginationContainer.style.display = 'block';
let paginationHtml = '<div class="pagination-controls">';
// Previous button
if (this.currentPage > 1) {
paginationHtml += `<button class="pagination-btn" data-page="${this.currentPage - 1}"> Previous</button>`;
}
// Page numbers (show max 7 pages)
const maxVisiblePages = 7;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(this.totalPages, startPage + maxVisiblePages - 1);
// Adjust if we're near the end
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// First page and ellipsis
if (startPage > 1) {
paginationHtml += `<button class="pagination-btn" data-page="1">1</button>`;
if (startPage > 2) {
paginationHtml += '<span class="pagination-ellipsis">...</span>';
}
}
// Page numbers
for (let i = startPage; i <= endPage; i++) {
const isActive = i === this.currentPage ? 'active' : '';
paginationHtml += `<button class="pagination-btn ${isActive}" data-page="${i}">${i}</button>`;
}
// Last page and ellipsis
if (endPage < this.totalPages) {
if (endPage < this.totalPages - 1) {
paginationHtml += '<span class="pagination-ellipsis">...</span>';
}
paginationHtml += `<button class="pagination-btn" data-page="${this.totalPages}">${this.totalPages}</button>`;
}
// Next button
if (this.currentPage < this.totalPages) {
paginationHtml += `<button class="pagination-btn" data-page="${this.currentPage + 1}">Next </button>`;
}
paginationHtml += '</div>';
paginationContainer.innerHTML = paginationHtml;
// Add click handlers for pagination
paginationContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('pagination-btn') && e.target.dataset.page) {
this.goToPage(parseInt(e.target.dataset.page));
}
});
}
createPaginationContainer() {
let container = document.getElementById('cuts-pagination');
if (!container) {
container = document.createElement('div');
container.id = 'cuts-pagination';
container.className = 'cuts-pagination';
// Insert after cuts list
const cutsList = document.getElementById('cuts-list');
if (cutsList && cutsList.parentNode) {
cutsList.parentNode.insertBefore(container, cutsList.nextSibling);
}
}
return container;
}
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
this.renderCutsList();
// Scroll to top of cuts list
const cutsList = document.getElementById('cuts-list');
if (cutsList) {
cutsList.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
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');
}
// Toggle locations visibility on the cuts map
async toggleLocationsOnMap(show) {
if (!this.cutsMap) {
console.warn('Cuts map not initialized');
return;
}
try {
if (show) {
// Load and display locations on the map
const response = await fetch('/api/locations');
const data = await response.json();
if (data.success && data.locations) {
// Remove existing location markers if any
if (this.locationMarkers) {
this.locationMarkers.clearLayers();
} else {
this.locationMarkers = L.layerGroup().addTo(this.cutsMap);
}
// Add location markers to the map
data.locations.forEach(location => {
if (location.latitude && location.longitude) {
// Handle multiple possible field names for support level
const supportLevel = location.support_level ||
location['Support Level'] ||
location.supportLevel ||
location['support level'] ||
location.Support_Level ||
'unknown';
// Debug logging for first few locations
if (data.locations.indexOf(location) < 3) {
console.log('Location debug:', {
support_level: location.support_level,
'Support Level': location['Support Level'],
supportLevel: location.supportLevel,
'support level': location['support level'],
Support_Level: location.Support_Level,
finalSupportLevel: supportLevel,
allKeys: Object.keys(location)
});
}
const marker = L.circleMarker([location.latitude, location.longitude], {
radius: 8,
fillColor: this.getLocationColor(supportLevel),
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
// Add popup with location info
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 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 signSize = location.sign_size || location['Sign Size'] || '';
const notes = location.notes || location.Notes || '';
const supportLevelText = this.getSupportLevelText(supportLevel);
const popupContent = `
<div class="location-popup">
<div class="location-header">
<strong>${name}</strong>
<span class="support-level support-level-${supportLevel || 'unknown'}">${supportLevelText}</span>
</div>
<div class="location-address">
📍 ${address || 'No address available'}
</div>
${contact ? `<div class="location-contact">
📞 ${contact}
</div>` : ''}
<div class="location-details">
<div class="detail-row">
<span class="detail-label">Lawn Sign:</span>
<span class="detail-value">${hasSign}${signSize ? ` (${signSize})` : ''}</span>
</div>
${location.latitude && location.longitude ? `
<div class="detail-row">
<span class="detail-label">Coordinates:</span>
<span class="detail-value coordinates">${parseFloat(location.latitude).toFixed(6)}, ${parseFloat(location.longitude).toFixed(6)}</span>
</div>` : ''}
</div>
${notes ? `<div class="location-notes">
<strong>Notes:</strong> ${notes.length > 100 ? notes.substring(0, 100) + '...' : notes}
</div>` : ''}
</div>
`;
marker.bindPopup(popupContent, {
maxWidth: 300,
className: 'location-popup-container'
});
this.locationMarkers.addLayer(marker);
}
});
console.log(`Added ${data.locations.length} location markers to cuts map`);
} else {
console.warn('No locations data received');
}
} else {
// Hide locations
if (this.locationMarkers) {
this.cutsMap.removeLayer(this.locationMarkers);
this.locationMarkers = null;
}
console.log('Removed location markers from cuts map');
}
} catch (error) {
console.error('Error toggling locations on map:', error);
this.showStatus('Failed to load locations', 'error');
}
}
// Helper method to get color based on support level
getLocationColor(supportLevel) {
switch (String(supportLevel)) {
case '1': return '#28a745'; // Green - Strong Support
case '2': return '#ffc107'; // Yellow - Lean Support
case '3': return '#fd7e14'; // Orange - Lean Opposition
case '4': return '#dc3545'; // Red - Strong Opposition
default: return '#6c757d'; // Gray - Unknown
}
}
// Helper method to get support level text
getSupportLevelText(supportLevel) {
switch (String(supportLevel)) {
case '1': return 'Strong Support';
case '2': return 'Lean Support';
case '3': return 'Lean Opposition';
case '4': return 'Strong Opposition';
default: return 'Unknown';
}
}
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;
}