1905 lines
77 KiB
JavaScript
1905 lines
77 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|