3248 lines
133 KiB
JavaScript
3248 lines
133 KiB
JavaScript
/**
|
|
* Admin Cuts Management Module
|
|
* Handles cut creation, editing, and management in the admin panel
|
|
*/
|
|
|
|
// Cut Drawing Class (converted to regular JS)
|
|
class CutDrawing {
|
|
constructor(map) {
|
|
this.map = map;
|
|
this.vertices = [];
|
|
this.markers = [];
|
|
this.polyline = null;
|
|
this.polygon = null; // Add polygon preview
|
|
this.finalPolygon = null; // Final polygon after finishing
|
|
this.isDrawing = false;
|
|
this.onFinishCallback = null;
|
|
this.onCancelCallback = null;
|
|
this.currentColor = '#3388ff';
|
|
this.currentOpacity = 0.3;
|
|
}
|
|
|
|
startDrawing(onFinish, onCancel) {
|
|
if (this.isDrawing) {
|
|
this.cancelDrawing();
|
|
}
|
|
|
|
this.isDrawing = true;
|
|
this.onFinishCallback = onFinish;
|
|
this.onCancelCallback = onCancel;
|
|
this.vertices = [];
|
|
this.markers = [];
|
|
|
|
this.map.getContainer().style.cursor = 'crosshair';
|
|
this.map.on('click', this.onMapClick.bind(this));
|
|
this.map.doubleClickZoom.disable();
|
|
|
|
console.log('Cut drawing started - click to add points');
|
|
}
|
|
|
|
onMapClick(e) {
|
|
if (!this.isDrawing) return;
|
|
|
|
// Check if clicking on first vertex to close polygon
|
|
if (this.vertices.length >= 3) {
|
|
const firstVertex = this.vertices[0];
|
|
const clickPoint = this.map.latLngToContainerPoint(e.latlng);
|
|
const firstPoint = this.map.latLngToContainerPoint(firstVertex);
|
|
const distance = Math.sqrt(
|
|
Math.pow(clickPoint.x - firstPoint.x, 2) +
|
|
Math.pow(clickPoint.y - firstPoint.y, 2)
|
|
);
|
|
|
|
// If clicked within 15 pixels of first vertex, close the polygon
|
|
if (distance < 15) {
|
|
console.log('Closing polygon by clicking first vertex');
|
|
this.finishDrawing();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Add vertex marker with special styling for first vertex
|
|
const isFirstVertex = this.vertices.length === 0;
|
|
const marker = L.marker(e.latlng, {
|
|
icon: L.divIcon({
|
|
className: 'cut-vertex-marker' + (isFirstVertex ? ' first-vertex' : ''),
|
|
html: `<div class="vertex-point${isFirstVertex ? ' first' : ''}"></div>`,
|
|
iconSize: [12, 12],
|
|
iconAnchor: [6, 6]
|
|
}),
|
|
draggable: false
|
|
}).addTo(this.map);
|
|
|
|
// Add tooltip to first vertex after 3 points
|
|
if (isFirstVertex) {
|
|
marker.bindTooltip('Click to close polygon', {
|
|
permanent: false,
|
|
direction: 'top',
|
|
offset: [0, -10]
|
|
});
|
|
}
|
|
|
|
this.vertices.push(e.latlng);
|
|
this.markers.push(marker);
|
|
this.updatePolyline();
|
|
|
|
// Show tooltip on first vertex when we have enough points
|
|
if (this.vertices.length === 3 && this.markers[0]) {
|
|
this.markers[0].openTooltip();
|
|
}
|
|
|
|
// Call update callback if available
|
|
if (this.onUpdate) {
|
|
this.onUpdate();
|
|
}
|
|
|
|
console.log(`Added vertex ${this.vertices.length} at`, e.latlng);
|
|
}
|
|
|
|
updatePolyline() {
|
|
// Use stored color and opacity values
|
|
const color = this.currentColor;
|
|
const opacity = this.currentOpacity;
|
|
|
|
if (this.polyline) {
|
|
this.map.removeLayer(this.polyline);
|
|
}
|
|
|
|
if (this.polygon) {
|
|
this.map.removeLayer(this.polygon);
|
|
this.polygon = null;
|
|
}
|
|
|
|
if (this.vertices.length > 1) {
|
|
// Show polyline for incomplete polygon
|
|
this.polyline = L.polyline(this.vertices, {
|
|
color: color,
|
|
weight: 2,
|
|
dashArray: '5, 5',
|
|
opacity: 1.0 // Keep polyline stroke visible
|
|
}).addTo(this.map);
|
|
|
|
// Show preview polygon if we have 3+ vertices
|
|
if (this.vertices.length >= 3) {
|
|
this.polygon = L.polygon(this.vertices, {
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
weight: 2,
|
|
opacity: 0.8,
|
|
dashArray: '5, 5'
|
|
}).addTo(this.map);
|
|
|
|
// Add cut-polygon class and force inline style
|
|
if (this.polygon._path) {
|
|
this.polygon._path.classList.add('cut-polygon');
|
|
// Use setProperty with important flag for stronger override
|
|
this.polygon._path.style.setProperty('fill-opacity', opacity, 'important');
|
|
this.polygon._path.style.setProperty('fill', color, 'important');
|
|
console.log(`Created/updated polygon with opacity: ${opacity}`);
|
|
}
|
|
|
|
console.log('Created/updated polygon with opacity:', opacity);
|
|
}
|
|
}
|
|
}
|
|
|
|
finishDrawing() {
|
|
console.log('finishDrawing() called');
|
|
console.log('isDrawing:', this.isDrawing);
|
|
console.log('vertices count:', this.vertices.length);
|
|
|
|
if (!this.isDrawing) {
|
|
console.log('Not in drawing mode, returning null');
|
|
return null;
|
|
}
|
|
|
|
if (this.vertices.length < 3) {
|
|
alert('A cut must have at least 3 points. Click more points or cancel drawing.');
|
|
return null;
|
|
}
|
|
|
|
// Store vertices before cleanup
|
|
const finalVertices = [...this.vertices];
|
|
|
|
// Use stored color and opacity values
|
|
const color = this.currentColor;
|
|
const opacity = this.currentOpacity;
|
|
|
|
// Create polygon from vertices
|
|
const polygon = L.polygon(finalVertices, {
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
weight: 2,
|
|
opacity: 1.0 // Keep stroke visible
|
|
});
|
|
|
|
// Get GeoJSON and bounds
|
|
const geojson = polygon.toGeoJSON().geometry; // Get just the geometry part
|
|
const bounds = polygon.getBounds();
|
|
|
|
// Clean up drawing elements but keep the final polygon
|
|
this.cleanupDrawingElements();
|
|
|
|
// Add the final polygon to the map
|
|
this.finalPolygon = polygon.addTo(this.map);
|
|
|
|
// Call finish callback with the data
|
|
console.log('Calling finish callbacks...');
|
|
const callbackData = {
|
|
geojson: JSON.stringify(geojson),
|
|
bounds: JSON.stringify([
|
|
[bounds.getSouth(), bounds.getWest()],
|
|
[bounds.getNorth(), bounds.getEast()]
|
|
]),
|
|
vertexCount: finalVertices.length,
|
|
polygon: this.finalPolygon
|
|
};
|
|
|
|
if (this.onFinishCallback) {
|
|
console.log('Calling onFinishCallback');
|
|
this.onFinishCallback(callbackData);
|
|
} else {
|
|
console.log('No onFinishCallback set');
|
|
}
|
|
|
|
// Also call the onFinish callback if set
|
|
if (this.onFinish) {
|
|
console.log('Calling onFinish');
|
|
this.onFinish(callbackData);
|
|
} else {
|
|
console.log('No onFinish callback set');
|
|
}
|
|
|
|
console.log('Cut drawing finished with', finalVertices.length, 'vertices');
|
|
return geojson;
|
|
}
|
|
|
|
cancelDrawing() {
|
|
if (!this.isDrawing) return;
|
|
|
|
console.log('Cut drawing cancelled');
|
|
this.cleanup();
|
|
|
|
if (this.onCancelCallback) {
|
|
this.onCancelCallback();
|
|
}
|
|
|
|
// Also call the onCancel callback if set
|
|
if (this.onCancel) {
|
|
this.onCancel();
|
|
}
|
|
}
|
|
|
|
undoLastVertex() {
|
|
if (!this.isDrawing || this.vertices.length === 0) return;
|
|
|
|
this.vertices.pop();
|
|
const lastMarker = this.markers.pop();
|
|
if (lastMarker) {
|
|
this.map.removeLayer(lastMarker);
|
|
}
|
|
|
|
this.updatePolyline();
|
|
|
|
// Call update callback if available
|
|
if (this.onUpdate) {
|
|
this.onUpdate();
|
|
}
|
|
|
|
console.log('Removed last vertex, remaining:', this.vertices.length);
|
|
}
|
|
|
|
clearVertices() {
|
|
if (!this.isDrawing) return;
|
|
|
|
this.markers.forEach(marker => {
|
|
this.map.removeLayer(marker);
|
|
});
|
|
|
|
if (this.polyline) {
|
|
this.map.removeLayer(this.polyline);
|
|
this.polyline = null;
|
|
}
|
|
|
|
if (this.polygon) {
|
|
this.map.removeLayer(this.polygon);
|
|
this.polygon = null;
|
|
}
|
|
|
|
this.vertices = [];
|
|
this.markers = [];
|
|
|
|
// Call update callback if available
|
|
if (this.onUpdate) {
|
|
this.onUpdate();
|
|
}
|
|
|
|
console.log('Cleared all vertices');
|
|
}
|
|
|
|
cleanupDrawingElements() {
|
|
// Remove drawing elements but preserve final polygon
|
|
this.markers.forEach(marker => {
|
|
this.map.removeLayer(marker);
|
|
});
|
|
|
|
if (this.polyline) {
|
|
this.map.removeLayer(this.polyline);
|
|
}
|
|
|
|
if (this.polygon) {
|
|
this.map.removeLayer(this.polygon);
|
|
}
|
|
|
|
this.map.getContainer().style.cursor = '';
|
|
this.map.off('click', this.onMapClick);
|
|
this.map.doubleClickZoom.enable();
|
|
|
|
this.isDrawing = false;
|
|
this.vertices = [];
|
|
this.markers = [];
|
|
this.polyline = null;
|
|
this.polygon = null;
|
|
this.onFinishCallback = null;
|
|
this.onCancelCallback = null;
|
|
}
|
|
|
|
cleanup() {
|
|
this.markers.forEach(marker => {
|
|
this.map.removeLayer(marker);
|
|
});
|
|
|
|
if (this.polyline) {
|
|
this.map.removeLayer(this.polyline);
|
|
}
|
|
|
|
if (this.polygon) {
|
|
this.map.removeLayer(this.polygon);
|
|
}
|
|
|
|
if (this.finalPolygon) {
|
|
this.map.removeLayer(this.finalPolygon);
|
|
}
|
|
|
|
this.map.getContainer().style.cursor = '';
|
|
this.map.off('click', this.onMapClick);
|
|
this.map.doubleClickZoom.enable();
|
|
|
|
this.isDrawing = false;
|
|
this.vertices = [];
|
|
this.markers = [];
|
|
this.polyline = null;
|
|
this.polygon = null;
|
|
this.finalPolygon = null;
|
|
this.onFinishCallback = null;
|
|
this.onCancelCallback = null;
|
|
}
|
|
|
|
getState() {
|
|
return {
|
|
isDrawing: this.isDrawing,
|
|
vertexCount: this.vertices.length,
|
|
canFinish: this.vertices.length >= 3
|
|
};
|
|
}
|
|
|
|
// Add method to update current drawing style
|
|
updateDrawingStyle(color, opacity) {
|
|
this.currentColor = color;
|
|
this.currentOpacity = opacity;
|
|
|
|
console.log(`CutDrawing.updateDrawingStyle called with color: ${color}, opacity: ${opacity}`);
|
|
|
|
// Update polyline color
|
|
if (this.polyline) {
|
|
this.polyline.setStyle({ color: color });
|
|
}
|
|
|
|
// Update polygon if it exists
|
|
if (this.polygon) {
|
|
this.polygon.setStyle({
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
opacity: 0.8 // Border opacity
|
|
});
|
|
|
|
// Force inline style update and browser reflow
|
|
if (this.polygon._path) {
|
|
// Use setProperty with important flag for stronger override
|
|
this.polygon._path.style.setProperty('fill-opacity', opacity, 'important');
|
|
this.polygon._path.style.setProperty('fill', color, 'important');
|
|
|
|
// Force multiple reflows to ensure update
|
|
void this.polygon._path.offsetHeight;
|
|
void this.polygon._path.offsetWidth;
|
|
|
|
// Force repaint by temporarily changing a property
|
|
const oldDisplay = this.polygon._path.style.display;
|
|
this.polygon._path.style.display = 'none';
|
|
void this.polygon._path.offsetHeight;
|
|
this.polygon._path.style.display = oldDisplay;
|
|
}
|
|
|
|
// Also try Leaflet's internal redraw
|
|
if (this.polygon._updatePath) {
|
|
this.polygon._updatePath();
|
|
}
|
|
|
|
console.log(`Updated active drawing polygon with opacity: ${opacity}`);
|
|
} else {
|
|
// If no polygon exists but we have vertices, force a complete redraw
|
|
console.log('No polygon exists, forcing complete redraw');
|
|
this.forceRedraw();
|
|
}
|
|
}
|
|
|
|
clearPreview() {
|
|
// Clear any preview polygons but not the final polygon
|
|
if (this.polygon) {
|
|
this.map.removeLayer(this.polygon);
|
|
this.polygon = null;
|
|
}
|
|
|
|
if (this.polyline && this.isDrawing) {
|
|
this.map.removeLayer(this.polyline);
|
|
this.polyline = null;
|
|
}
|
|
}
|
|
|
|
// Force a complete redraw with current style settings
|
|
forceRedraw() {
|
|
if (this.vertices.length > 1) {
|
|
console.log('Forcing complete redraw with vertices:', this.vertices.length);
|
|
this.updatePolyline();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Admin Cuts Manager Class
|
|
class AdminCutsManager {
|
|
constructor() {
|
|
this.cutsMap = null;
|
|
this.cutDrawing = null;
|
|
this.currentCutId = null;
|
|
this.allCuts = [];
|
|
this.filteredCuts = [];
|
|
this.currentCutLayer = null;
|
|
this.isInitialized = false; // Add initialization flag
|
|
|
|
// Location management properties
|
|
this.currentCutLocations = [];
|
|
this.currentFilters = {};
|
|
this.locationMarkersLayer = null;
|
|
this.showingLocations = false;
|
|
|
|
// Bind event handler once to avoid issues with removing listeners
|
|
this.boundHandleCutActionClick = this.handleCutActionClick.bind(this);
|
|
}
|
|
|
|
async initialize() {
|
|
// Prevent double initialization
|
|
if (this.isInitialized) {
|
|
console.log('AdminCutsManager already initialized');
|
|
return;
|
|
}
|
|
|
|
console.log('Initializing admin cuts manager...');
|
|
|
|
// Initialize map first
|
|
this.initializeMap();
|
|
|
|
// Initialize form first
|
|
this.initializeFormState();
|
|
|
|
// Initialize cuts list element
|
|
this.cutsList = document.getElementById('cuts-list');
|
|
|
|
// Initialize drawing
|
|
this.initializeDrawing();
|
|
|
|
// Load existing cuts
|
|
await this.loadCuts();
|
|
|
|
// Set initialized flag BEFORE logging to prevent re-entry
|
|
this.isInitialized = true;
|
|
|
|
console.log('Admin cuts manager initialized');
|
|
}
|
|
|
|
initializeFormState() {
|
|
console.log('Initializing form state...');
|
|
|
|
// Set up form elements
|
|
this.form = document.getElementById('cut-form');
|
|
if (!this.form) {
|
|
console.error('Cut form not found');
|
|
return;
|
|
}
|
|
|
|
// Keep form enabled at all times - users should be able to fill properties anytime
|
|
// Only disable the save button until we have geometry
|
|
const saveCutBtn = document.getElementById('save-cut-btn');
|
|
if (saveCutBtn) {
|
|
saveCutBtn.disabled = true;
|
|
}
|
|
|
|
// Set up form submission
|
|
this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
|
|
|
// NOTE: Opacity event listener is set up in setupFormControls() to avoid conflicts
|
|
|
|
// Set up other form controls
|
|
this.setupFormControls();
|
|
|
|
// Set up toolbar controls for drawing
|
|
this.setupToolbarControls();
|
|
|
|
console.log('Form state initialized - form inputs enabled, save button disabled until geometry complete');
|
|
}
|
|
|
|
// Add method to update preview style when color/opacity changes
|
|
updatePreviewStyle() {
|
|
// Simplified update method - uses toolbar values
|
|
console.log('Note: updatePreviewStyle() called but now using toolbar controls');
|
|
// This method is no longer needed since toolbar controls update drawing style directly
|
|
}
|
|
|
|
// Method that accepts direct values to avoid DOM reading issues (kept for compatibility)
|
|
updatePreviewStyleWithValues(colorOverride = null, opacityOverride = null) {
|
|
const color = colorOverride || this.getCurrentColor();
|
|
const opacity = opacityOverride !== null ? opacityOverride : this.getCurrentOpacity();
|
|
|
|
console.log('updatePreviewStyleWithValues called with:', color, opacity);
|
|
|
|
// Update drawing style if drawing is active
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
}
|
|
}
|
|
|
|
// Method to apply styles to all relevant layers
|
|
applyStyleToLayers(color, opacity) {
|
|
console.log('applyStyleToLayers called with color:', color, 'opacity:', opacity);
|
|
|
|
// First, update the drawing tool's current style if drawing is active
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
}
|
|
|
|
// Update any preview layer (GeoJSON) - this is the critical fix
|
|
if (this.previewLayer) {
|
|
this.updateLayerStyle(this.previewLayer, color, opacity);
|
|
console.log('Preview GeoJSON layer style updated with opacity:', opacity);
|
|
}
|
|
|
|
// Update current cut layer if it exists - Enhanced handling
|
|
if (this.currentCutLayer) {
|
|
this.updateLayerStyle(this.currentCutLayer, color, opacity);
|
|
console.log('Updated currentCutLayer with opacity:', opacity);
|
|
}
|
|
|
|
// If preview layer doesn't exist but we have drawing data, refresh the preview
|
|
if (!this.previewLayer && this.currentDrawingData) {
|
|
console.log('No preview layer found, refreshing preview with drawing data');
|
|
this.updateDrawingPreview(this.currentDrawingData);
|
|
}
|
|
}
|
|
|
|
// New unified method to update any layer style
|
|
updateLayerStyle(layer, color, opacity) {
|
|
if (!layer) return;
|
|
|
|
// Update layer options
|
|
layer.options.fillOpacity = opacity;
|
|
layer.options.fillColor = color;
|
|
layer.options.color = color;
|
|
|
|
// Apply new style
|
|
layer.setStyle({
|
|
fillColor: color,
|
|
color: color,
|
|
fillOpacity: opacity,
|
|
opacity: 0.8 // Border opacity
|
|
});
|
|
|
|
// Force update on the path element(s)
|
|
if (layer._path) {
|
|
layer._path.style.setProperty('fill-opacity', opacity, 'important');
|
|
layer._path.style.setProperty('fill', color, 'important');
|
|
layer._path.style.setProperty('stroke', color, 'important');
|
|
|
|
// Force browser to recognize the change
|
|
layer._path.style.opacity = ''; // Clear any overall opacity
|
|
void layer._path.offsetHeight; // Force reflow
|
|
}
|
|
|
|
// Handle renderer sub-layers
|
|
if (layer._renderer && layer._renderer._container) {
|
|
const paths = layer._renderer._container.querySelectorAll('path');
|
|
paths.forEach(path => {
|
|
path.style.setProperty('fill-opacity', opacity, 'important');
|
|
path.style.setProperty('fill', color, 'important');
|
|
path.style.setProperty('stroke', color, 'important');
|
|
|
|
// Force browser to recognize the change
|
|
path.style.opacity = ''; // Clear any overall opacity
|
|
void path.offsetHeight; // Force reflow
|
|
});
|
|
}
|
|
|
|
// Handle GeoJSON layers and layer groups
|
|
if (layer.eachLayer) {
|
|
// It's a layer group or GeoJSON - iterate through each feature
|
|
layer.eachLayer((subLayer) => {
|
|
if (subLayer.setStyle) {
|
|
subLayer.setStyle({
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
opacity: 0.8
|
|
});
|
|
|
|
// Force inline styles on sub-layer paths
|
|
if (subLayer._path) {
|
|
subLayer._path.style.setProperty('fill-opacity', opacity, 'important');
|
|
subLayer._path.style.setProperty('fill', color, 'important');
|
|
subLayer._path.style.setProperty('stroke', color, 'important');
|
|
subLayer._path.style.opacity = '';
|
|
void subLayer._path.offsetHeight;
|
|
}
|
|
|
|
// Add CSS class to identify cut polygons
|
|
const pathElement = subLayer.getElement();
|
|
if (pathElement) {
|
|
pathElement.classList.add('cut-polygon');
|
|
pathElement.style.setProperty('fill-opacity', opacity, 'important');
|
|
console.log('Added cut-polygon class to sub-layer path element');
|
|
}
|
|
// Force DOM update immediately
|
|
this.forceLayerRedraw(subLayer);
|
|
}
|
|
});
|
|
|
|
// Also force a redraw of the entire layer group
|
|
if (layer.redraw) {
|
|
layer.redraw();
|
|
}
|
|
} else if (layer.setStyle) {
|
|
// It's a single layer (Leaflet Polygon)
|
|
layer.setStyle({
|
|
fillColor: color,
|
|
color: color,
|
|
fillOpacity: opacity,
|
|
opacity: 0.8
|
|
});
|
|
// Add CSS class to identify cut polygons
|
|
const pathElement = layer.getElement();
|
|
if (pathElement) {
|
|
pathElement.classList.add('cut-polygon');
|
|
console.log('Added cut-polygon class to single layer path element');
|
|
}
|
|
// Force DOM update immediately
|
|
this.forceLayerRedraw(layer);
|
|
}
|
|
}
|
|
|
|
// New method to force layer redraw - addresses browser rendering issues
|
|
forceLayerRedraw(layer) {
|
|
if (layer._path) {
|
|
// Direct SVG path manipulation for immediate visual update
|
|
const path = layer._path;
|
|
const targetOpacity = layer.options.fillOpacity;
|
|
const targetColor = layer.options.fillColor || layer.options.color;
|
|
|
|
console.log('forceLayerRedraw called:');
|
|
console.log(' - layer.options.fillOpacity:', targetOpacity);
|
|
console.log(' - layer.options.fillColor:', targetColor);
|
|
console.log(' - layer._path exists:', !!path);
|
|
|
|
// Set the attribute directly on the SVG element
|
|
path.setAttribute('fill-opacity', targetOpacity);
|
|
path.setAttribute('fill', targetColor);
|
|
|
|
// Also try setting as CSS style with important flag for better browser compatibility
|
|
path.style.setProperty('fill-opacity', targetOpacity, 'important');
|
|
path.style.setProperty('fill', targetColor, 'important');
|
|
|
|
// Force browser reflow by temporarily changing a property
|
|
const originalDisplay = path.style.display;
|
|
path.style.display = 'none';
|
|
|
|
// Use requestAnimationFrame for better timing
|
|
requestAnimationFrame(() => {
|
|
path.style.display = originalDisplay;
|
|
|
|
// Double-check the attribute was set
|
|
const finalOpacity = path.getAttribute('fill-opacity');
|
|
const finalColor = path.getAttribute('fill');
|
|
const styleOpacity = path.style.fillOpacity;
|
|
|
|
console.log('forceLayerRedraw completed:');
|
|
console.log(' - target opacity:', targetOpacity);
|
|
console.log(' - target color:', targetColor);
|
|
console.log(' - SVG attr opacity result:', finalOpacity);
|
|
console.log(' - SVG attr color result:', finalColor);
|
|
console.log(' - CSS style opacity result:', styleOpacity);
|
|
|
|
// If attributes don't match, try one more time
|
|
if (finalOpacity !== targetOpacity.toString()) {
|
|
path.setAttribute('fill-opacity', targetOpacity);
|
|
path.style.setProperty('fill-opacity', targetOpacity, 'important');
|
|
console.log(' - Re-applied fill-opacity attribute and style');
|
|
}
|
|
});
|
|
} else {
|
|
console.log('forceLayerRedraw: no _path found on layer');
|
|
}
|
|
}
|
|
|
|
setupFormControls() {
|
|
// Set up start drawing button
|
|
const startDrawingBtn = document.getElementById('start-drawing-btn');
|
|
if (startDrawingBtn) {
|
|
// Remove any existing listeners first
|
|
startDrawingBtn.removeEventListener('click', this.boundHandleStartDrawing);
|
|
// Create bound method if it doesn't exist
|
|
if (!this.boundHandleStartDrawing) {
|
|
this.boundHandleStartDrawing = this.handleStartDrawing.bind(this);
|
|
}
|
|
startDrawingBtn.addEventListener('click', this.boundHandleStartDrawing);
|
|
}
|
|
|
|
// Set up reset form button
|
|
const resetFormBtn = document.getElementById('reset-form-btn');
|
|
if (resetFormBtn) {
|
|
resetFormBtn.addEventListener('click', () => this.resetForm());
|
|
}
|
|
|
|
// Set up cancel edit button
|
|
const cancelEditBtn = document.getElementById('cancel-edit-btn');
|
|
if (cancelEditBtn) {
|
|
cancelEditBtn.addEventListener('click', () => this.cancelEdit());
|
|
}
|
|
|
|
// Set up refresh cuts button
|
|
const refreshCutsBtn = document.getElementById('refresh-cuts-btn');
|
|
if (refreshCutsBtn) {
|
|
refreshCutsBtn.addEventListener('click', () => this.loadCuts());
|
|
}
|
|
|
|
// Set up export button
|
|
const exportCutsBtn = document.getElementById('export-cuts-btn');
|
|
if (exportCutsBtn) {
|
|
exportCutsBtn.addEventListener('click', () => this.exportCuts());
|
|
}
|
|
|
|
// Set up import file input
|
|
const importCutsFile = document.getElementById('import-cuts-file');
|
|
if (importCutsFile) {
|
|
importCutsFile.addEventListener('change', (e) => this.handleImport(e));
|
|
}
|
|
|
|
// Set up search and filter
|
|
const searchInput = document.getElementById('cuts-search');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', () => this.filterCuts());
|
|
}
|
|
|
|
const categoryFilter = document.getElementById('cuts-category-filter');
|
|
if (categoryFilter) {
|
|
categoryFilter.addEventListener('change', () => this.filterCuts());
|
|
}
|
|
|
|
// NOTE: Color and opacity controls moved to toolbar for real-time feedback
|
|
// Form-based color/opacity controls are no longer used
|
|
|
|
// Add drawing toolbar button handlers
|
|
const finishDrawingBtn = document.getElementById('finish-cut-btn'); // Fixed ID
|
|
if (finishDrawingBtn) {
|
|
finishDrawingBtn.addEventListener('click', () => {
|
|
console.log('Finish button clicked');
|
|
if (this.cutDrawing) {
|
|
console.log('Cut drawing exists, calling finishDrawing()');
|
|
console.log('Drawing state:', this.cutDrawing.getState());
|
|
this.cutDrawing.finishDrawing();
|
|
} else {
|
|
console.error('Cut drawing not initialized');
|
|
}
|
|
});
|
|
} else {
|
|
console.error('Finish drawing button not found');
|
|
}
|
|
|
|
const undoVertexBtn = document.getElementById('undo-vertex-btn');
|
|
if (undoVertexBtn) {
|
|
undoVertexBtn.addEventListener('click', () => {
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.undoLastVertex();
|
|
this.updateDrawingUI();
|
|
}
|
|
});
|
|
}
|
|
|
|
const clearVerticesBtn = document.getElementById('clear-vertices-btn');
|
|
if (clearVerticesBtn) {
|
|
clearVerticesBtn.addEventListener('click', () => {
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.clearVertices();
|
|
this.updateDrawingUI();
|
|
}
|
|
});
|
|
}
|
|
|
|
const cancelDrawingBtn = document.getElementById('cancel-cut-btn'); // Fixed ID
|
|
if (cancelDrawingBtn) {
|
|
cancelDrawingBtn.addEventListener('click', () => {
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.cancelDrawing();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Location management event listeners
|
|
this.setupLocationManagementControls();
|
|
}
|
|
|
|
// Set up toolbar controls for real-time drawing feedback
|
|
setupToolbarControls() {
|
|
const colorPicker = document.getElementById('toolbar-color');
|
|
const opacitySlider = document.getElementById('toolbar-opacity');
|
|
const opacityDisplay = document.getElementById('toolbar-opacity-display');
|
|
|
|
console.log('Setting up toolbar controls...', {
|
|
colorPicker: !!colorPicker,
|
|
opacitySlider: !!opacitySlider,
|
|
opacityDisplay: !!opacityDisplay
|
|
});
|
|
|
|
if (colorPicker) {
|
|
colorPicker.addEventListener('input', (e) => {
|
|
const color = e.target.value;
|
|
console.log('Toolbar color changed to:', color);
|
|
|
|
// Update drawing style immediately
|
|
if (this.cutDrawing) {
|
|
const opacity = this.getCurrentOpacity();
|
|
console.log('Updating drawing style with color:', color, 'opacity:', opacity);
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (opacitySlider && opacityDisplay) {
|
|
opacitySlider.addEventListener('input', (e) => {
|
|
const opacity = parseFloat(e.target.value);
|
|
const percentage = Math.round(opacity * 100);
|
|
|
|
opacityDisplay.textContent = percentage + '%';
|
|
console.log('Toolbar opacity changed to:', opacity, 'percentage:', percentage);
|
|
|
|
// Update drawing style immediately
|
|
if (this.cutDrawing) {
|
|
const color = this.getCurrentColor();
|
|
console.log('Updating drawing style with color:', color, 'opacity:', opacity);
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
} else {
|
|
console.warn('cutDrawing instance not available');
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('Toolbar controls setup complete');
|
|
}
|
|
|
|
// Helper methods to get current toolbar values
|
|
getCurrentColor() {
|
|
const colorPicker = document.getElementById('toolbar-color');
|
|
return colorPicker ? colorPicker.value : '#3388ff';
|
|
}
|
|
|
|
getCurrentOpacity() {
|
|
const opacitySlider = document.getElementById('toolbar-opacity');
|
|
return opacitySlider ? parseFloat(opacitySlider.value) : 0.3;
|
|
}
|
|
|
|
// Force update the drawing style - useful for debugging or manual refresh
|
|
forceUpdateDrawingStyle() {
|
|
if (this.cutDrawing) {
|
|
const color = this.getCurrentColor();
|
|
const opacity = this.getCurrentOpacity();
|
|
console.log('Force updating drawing style with color:', color, 'opacity:', opacity);
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
} else {
|
|
console.warn('cutDrawing instance not available for force update');
|
|
}
|
|
}
|
|
|
|
// Sync toolbar display values with actual slider values
|
|
syncToolbarDisplayValues() {
|
|
const opacitySlider = document.getElementById('toolbar-opacity');
|
|
const opacityDisplay = document.getElementById('toolbar-opacity-display');
|
|
|
|
if (opacitySlider && opacityDisplay) {
|
|
const opacity = parseFloat(opacitySlider.value);
|
|
const percentage = Math.round(opacity * 100);
|
|
opacityDisplay.textContent = percentage + '%';
|
|
console.log('Synced opacity display to:', percentage + '%');
|
|
}
|
|
|
|
// Also sync any existing preview layers with current toolbar values
|
|
const color = this.getCurrentColor();
|
|
const opacity = this.getCurrentOpacity();
|
|
|
|
if (this.previewLayer) {
|
|
console.log('Syncing preview layer with toolbar values:', color, opacity);
|
|
this.updateLayerStyle(this.previewLayer, color, opacity);
|
|
}
|
|
|
|
if (this.currentCutLayer) {
|
|
console.log('Syncing current cut layer with toolbar values:', color, opacity);
|
|
this.updateLayerStyle(this.currentCutLayer, color, opacity);
|
|
}
|
|
}
|
|
|
|
// Remove the form disable/enable methods since we keep form enabled at all times
|
|
updateDrawingUI() {
|
|
if (!this.cutDrawing) return;
|
|
|
|
const state = this.cutDrawing.getState();
|
|
const vertexCount = document.getElementById('vertex-count');
|
|
const finishBtn = document.getElementById('finish-cut-btn'); // Fixed ID
|
|
const undoBtn = document.getElementById('undo-vertex-btn');
|
|
|
|
if (vertexCount) {
|
|
vertexCount.textContent = `${state.vertexCount} points`;
|
|
}
|
|
|
|
if (finishBtn) {
|
|
finishBtn.disabled = !state.canFinish;
|
|
}
|
|
|
|
if (undoBtn) {
|
|
undoBtn.disabled = state.vertexCount === 0;
|
|
}
|
|
}
|
|
|
|
async initializeMap() {
|
|
const mapContainer = document.getElementById('cuts-map');
|
|
if (!mapContainer) {
|
|
console.error('Cuts map container not found');
|
|
return;
|
|
}
|
|
|
|
// Check if map is already initialized
|
|
if (this.cutsMap) {
|
|
console.log('Cuts map already initialized');
|
|
return;
|
|
}
|
|
|
|
// Check if container already has a map instance
|
|
if (mapContainer._leaflet_id) {
|
|
console.log('Map container already has a Leaflet instance, cleaning up...');
|
|
// Try to find and remove existing map
|
|
const existingMap = L.map.hasOwnProperty(mapContainer._leaflet_id) ? L.map[mapContainer._leaflet_id] : null;
|
|
if (existingMap) {
|
|
existingMap.remove();
|
|
}
|
|
delete mapContainer._leaflet_id;
|
|
}
|
|
|
|
// Initialize map
|
|
this.cutsMap = L.map('cuts-map').setView([53.5461, -113.4938], 11);
|
|
|
|
// Add tile layer
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(this.cutsMap);
|
|
|
|
console.log('Cuts map initialized');
|
|
}
|
|
|
|
initializeDrawing() {
|
|
this.cutDrawing = new CutDrawing(this.cutsMap);
|
|
|
|
// Ensure clean state
|
|
this.cutDrawing.isDrawing = false;
|
|
|
|
// Bind drawing events
|
|
this.cutDrawing.onFinish = (data) => this.handleDrawingFinished(data);
|
|
this.cutDrawing.onCancel = () => this.handleDrawingCancelled();
|
|
this.cutDrawing.onUpdate = () => this.updateDrawingUI();
|
|
|
|
console.log('Drawing initialized, isDrawing:', this.cutDrawing.isDrawing);
|
|
}
|
|
|
|
handleStartDrawing() {
|
|
console.log('handleStartDrawing called, current drawing state:', this.cutDrawing.isDrawing);
|
|
|
|
// Prevent double-click issues by adding a small delay check
|
|
if (this.handleStartDrawing._processing) {
|
|
console.log('Already processing start drawing, ignoring...');
|
|
return;
|
|
}
|
|
|
|
this.handleStartDrawing._processing = true;
|
|
|
|
try {
|
|
if (this.cutDrawing.isDrawing) {
|
|
console.log('Already drawing, canceling...');
|
|
this.cutDrawing.cancelDrawing();
|
|
return;
|
|
}
|
|
|
|
console.log('Starting new drawing...');
|
|
|
|
// Get current toolbar values instead of form values
|
|
const color = this.getCurrentColor();
|
|
const opacity = this.getCurrentOpacity();
|
|
|
|
console.log('Starting new drawing with color:', color, 'opacity:', opacity);
|
|
|
|
// Update the drawing tool with current style
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.currentColor = color;
|
|
this.cutDrawing.currentOpacity = opacity;
|
|
// Force update the drawing style immediately
|
|
this.cutDrawing.updateDrawingStyle(color, opacity);
|
|
}
|
|
|
|
// Clear any existing preview layers
|
|
if (this.previewLayer) {
|
|
this.cutsMap.removeLayer(this.previewLayer);
|
|
this.previewLayer = null;
|
|
}
|
|
|
|
// Clear any existing current cut layer
|
|
if (this.currentCutLayer) {
|
|
if (this.currentCutLayer.remove) {
|
|
this.currentCutLayer.remove();
|
|
} else {
|
|
this.cutsMap.removeLayer(this.currentCutLayer);
|
|
}
|
|
this.currentCutLayer = null;
|
|
}
|
|
|
|
// Clear form data
|
|
this.currentDrawingData = null;
|
|
document.getElementById('cut-geojson').value = '';
|
|
document.getElementById('cut-bounds').value = '';
|
|
|
|
// Clear any existing preview from the drawing tool
|
|
if (this.cutDrawing && this.cutDrawing.clearPreview) {
|
|
this.cutDrawing.clearPreview();
|
|
}
|
|
|
|
// Update button text
|
|
const startDrawingBtn = document.getElementById('start-drawing-btn');
|
|
if (startDrawingBtn) {
|
|
startDrawingBtn.textContent = 'Cancel Drawing';
|
|
}
|
|
|
|
this.cutDrawing.startDrawing(
|
|
(data) => this.handleDrawingFinished(data),
|
|
() => this.handleDrawingCancelled()
|
|
);
|
|
|
|
// Show drawing toolbar
|
|
const toolbar = document.getElementById('cut-drawing-toolbar');
|
|
if (toolbar) {
|
|
toolbar.classList.add('active');
|
|
// Sync toolbar display values and apply to any existing layers
|
|
this.syncToolbarDisplayValues();
|
|
}
|
|
|
|
// Update UI
|
|
this.updateDrawingUI();
|
|
} finally {
|
|
// Clear the processing flag after a short delay
|
|
setTimeout(() => {
|
|
this.handleStartDrawing._processing = false;
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
handleDrawingFinished(drawingData) {
|
|
console.log('handleDrawingFinished() called with data:', drawingData);
|
|
|
|
// Store the drawing data
|
|
this.currentDrawingData = drawingData;
|
|
|
|
// Hide drawing toolbar
|
|
document.getElementById('cut-drawing-toolbar').classList.remove('active');
|
|
|
|
// Store geojson and bounds in hidden form fields
|
|
document.getElementById('cut-geojson').value = drawingData.geojson;
|
|
document.getElementById('cut-bounds').value = drawingData.bounds;
|
|
|
|
// Store the geojson in form dataset for form submission
|
|
const form = document.getElementById('cut-form');
|
|
if (form) {
|
|
form.dataset.geojson = drawingData.geojson;
|
|
form.dataset.bounds = drawingData.bounds;
|
|
}
|
|
|
|
// Store the polygon reference for later use
|
|
if (drawingData.polygon) {
|
|
this.currentCutLayer = drawingData.polygon;
|
|
}
|
|
|
|
// Update form title
|
|
const titleElement = document.getElementById('cut-form-title');
|
|
if (titleElement) {
|
|
titleElement.textContent = 'Cut Properties - Ready to Save';
|
|
}
|
|
|
|
// Enable the save button now that we have geometry
|
|
const saveCutBtn = document.getElementById('save-cut-btn');
|
|
if (saveCutBtn) {
|
|
saveCutBtn.disabled = false;
|
|
}
|
|
|
|
// Update start drawing button text
|
|
const startDrawingBtn = document.getElementById('start-drawing-btn');
|
|
if (startDrawingBtn) {
|
|
startDrawingBtn.textContent = 'Redraw Polygon';
|
|
}
|
|
|
|
// Update preview with the drawn polygon using current toolbar values
|
|
this.updateDrawingPreview(drawingData);
|
|
|
|
// Force immediate style update with current toolbar values
|
|
const color = this.getCurrentColor();
|
|
const opacity = this.getCurrentOpacity();
|
|
console.log('handleDrawingFinished: Applying toolbar values - color:', color, 'opacity:', opacity);
|
|
|
|
// Apply to the preview layer immediately
|
|
if (this.previewLayer) {
|
|
this.updateLayerStyle(this.previewLayer, color, opacity);
|
|
}
|
|
|
|
// Also update the polygon from drawing data if it exists
|
|
if (drawingData.polygon) {
|
|
this.updateLayerStyle(drawingData.polygon, color, opacity);
|
|
}
|
|
|
|
this.showStatus('Cut drawing completed. Fill in the properties and save.', 'success');
|
|
}
|
|
|
|
handleDrawingCancelled() {
|
|
console.log('handleDrawingCancelled called');
|
|
|
|
const toolbar = document.getElementById('cut-drawing-toolbar');
|
|
if (toolbar) {
|
|
toolbar.classList.remove('active');
|
|
}
|
|
|
|
// Clear stored drawing data
|
|
this.currentDrawingData = null;
|
|
|
|
const geojsonField = document.getElementById('cut-geojson');
|
|
const boundsField = document.getElementById('cut-bounds');
|
|
if (geojsonField) geojsonField.value = '';
|
|
if (boundsField) boundsField.value = '';
|
|
|
|
// Clear form dataset
|
|
const form = document.getElementById('cut-form');
|
|
if (form) {
|
|
delete form.dataset.geojson;
|
|
delete form.dataset.bounds;
|
|
}
|
|
|
|
const saveCutBtn = document.getElementById('save-cut-btn');
|
|
if (saveCutBtn) {
|
|
saveCutBtn.disabled = true;
|
|
}
|
|
|
|
// Reset start drawing button text
|
|
const startDrawingBtn = document.getElementById('start-drawing-btn');
|
|
if (startDrawingBtn) {
|
|
startDrawingBtn.textContent = 'Start Drawing';
|
|
}
|
|
|
|
console.log('Drawing cancelled, state reset');
|
|
this.showStatus('Cut drawing cancelled', 'info');
|
|
}
|
|
|
|
reset() {
|
|
console.log('Resetting cut form and drawing...');
|
|
|
|
// Reset the drawing if active
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.reset();
|
|
}
|
|
|
|
// Reset form
|
|
if (this.cutForm) {
|
|
this.cutForm.reset();
|
|
|
|
// Set default values
|
|
const colorInput = document.getElementById('cut-color');
|
|
const opacityInput = document.getElementById('cut-opacity');
|
|
|
|
if (colorInput) colorInput.value = '#3388ff';
|
|
if (opacityInput) opacityInput.value = '0.3';
|
|
|
|
this.updateColorValue();
|
|
this.updateOpacityValue();
|
|
}
|
|
|
|
// Hide drawing toolbar
|
|
const drawingToolbar = document.getElementById('cut-drawing-toolbar');
|
|
if (drawingToolbar) {
|
|
drawingToolbar.classList.remove('active');
|
|
}
|
|
|
|
// Reset buttons
|
|
const startDrawingBtn = document.getElementById('start-drawing-btn');
|
|
if (startDrawingBtn) {
|
|
startDrawingBtn.textContent = 'Start Drawing';
|
|
startDrawingBtn.classList.remove('danger');
|
|
}
|
|
|
|
// Hide location management panel
|
|
this.hideLocationManagement();
|
|
|
|
console.log('Cut form and drawing reset complete');
|
|
}
|
|
|
|
async handleFormSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
console.log('Form submitted!');
|
|
|
|
const formData = new FormData(this.form); // Use this.form instead of this.cutForm
|
|
console.log('Form data entries:');
|
|
for (let [key, value] of formData.entries()) {
|
|
console.log(`${key}: ${value}`);
|
|
}
|
|
|
|
const cutData = {
|
|
name: formData.get('name'), // Use the actual HTML name attributes
|
|
description: formData.get('description'),
|
|
color: this.getCurrentColor(), // Get from toolbar instead of form
|
|
opacity: this.getCurrentOpacity(), // Get from toolbar instead of form
|
|
category: formData.get('category'),
|
|
is_public: formData.has('is_public'),
|
|
is_official: formData.has('is_official')
|
|
};
|
|
|
|
// Add the geojson and bounds from stored data
|
|
if (this.currentDrawingData || event.target.dataset.geojson) {
|
|
cutData.geojson = this.currentDrawingData?.geojson || event.target.dataset.geojson;
|
|
cutData.bounds = this.currentDrawingData?.bounds || event.target.dataset.bounds;
|
|
} else if (this.editingCutId) {
|
|
// If editing and no new drawing, keep existing geometry
|
|
const existingCut = this.allCuts.find(c => c.id === this.editingCutId);
|
|
if (existingCut) {
|
|
cutData.geojson = existingCut.geojson;
|
|
cutData.bounds = existingCut.bounds;
|
|
}
|
|
} else {
|
|
// Also try to get from hidden form fields as fallback
|
|
cutData.geojson = formData.get('geojson');
|
|
cutData.bounds = formData.get('bounds');
|
|
}
|
|
|
|
console.log('Cut data:', cutData);
|
|
|
|
if (!cutData.name || !cutData.geojson) {
|
|
this.showStatus('Name and geometry are required', 'error');
|
|
console.log('Validation failed - missing name or geojson');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let result;
|
|
if (this.editingCutId) {
|
|
result = await this.updateCut(this.editingCutId, cutData);
|
|
} else {
|
|
result = await this.createCut(cutData);
|
|
}
|
|
|
|
if (result) {
|
|
this.resetForm();
|
|
this.currentDrawingData = null;
|
|
await this.loadCuts();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving cut:', error);
|
|
this.showStatus('Failed to save cut', 'error');
|
|
}
|
|
}
|
|
|
|
// Add a new method to update the drawing preview
|
|
updateDrawingPreview(drawingData) {
|
|
if (!drawingData || !drawingData.geojson) return;
|
|
|
|
try {
|
|
const geojson = JSON.parse(drawingData.geojson);
|
|
|
|
// Remove any existing preview layer
|
|
if (this.previewLayer) {
|
|
this.cutsMap.removeLayer(this.previewLayer);
|
|
this.previewLayer = null;
|
|
}
|
|
|
|
// Get current toolbar colors - this is the key fix
|
|
const color = this.getCurrentColor();
|
|
const opacity = this.getCurrentOpacity();
|
|
|
|
console.log('updateDrawingPreview: Using toolbar values - color:', color, 'opacity:', opacity);
|
|
|
|
// Create the GeoJSON layer with a static style object (not a function)
|
|
// This allows setStyle() to work properly later
|
|
this.previewLayer = L.geoJSON(geojson, {
|
|
style: {
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
weight: 2,
|
|
opacity: 0.8, // Change border opacity to 0.8 for consistency
|
|
className: 'cut-polygon',
|
|
dashArray: '3, 3'
|
|
}
|
|
}).addTo(this.cutsMap);
|
|
|
|
// Add the cut-polygon CSS class to the path element and force inline styles
|
|
if (this.previewLayer._layers) {
|
|
Object.values(this.previewLayer._layers).forEach(layer => {
|
|
if (layer._path) {
|
|
layer._path.classList.add('cut-polygon');
|
|
// Force the fill-opacity inline style with important flag
|
|
layer._path.style.setProperty('fill-opacity', opacity, 'important');
|
|
layer._path.style.setProperty('fill', color, 'important');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Also check if we need to access sub-layers
|
|
if (this.previewLayer._renderer && this.previewLayer._renderer._container) {
|
|
const paths = this.previewLayer._renderer._container.querySelectorAll('path');
|
|
paths.forEach(path => {
|
|
path.classList.add('cut-polygon');
|
|
// Force the fill-opacity inline style on all paths with important flag
|
|
path.style.setProperty('fill-opacity', opacity, 'important');
|
|
path.style.setProperty('fill', color, 'important');
|
|
});
|
|
}
|
|
|
|
// Force initial style application using our unified method
|
|
this.updateLayerStyle(this.previewLayer, color, opacity);
|
|
|
|
console.log('Drawing preview updated with opacity:', opacity);
|
|
|
|
// Fit map to bounds if available
|
|
if (drawingData.bounds) {
|
|
const bounds = JSON.parse(drawingData.bounds);
|
|
this.cutsMap.fitBounds(bounds, { padding: [20, 20] });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating drawing preview:', error);
|
|
}
|
|
}
|
|
|
|
// Method to refresh preview with current drawing data and form values
|
|
refreshPreview() {
|
|
if (this.currentDrawingData) {
|
|
console.log('Refreshing preview with current form values');
|
|
this.updateDrawingPreview(this.currentDrawingData);
|
|
}
|
|
}
|
|
|
|
async createCut(cutData) {
|
|
try {
|
|
const response = await fetch('/api/cuts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(cutData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Failed to create cut: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
this.showStatus('Cut created successfully', 'success');
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error creating cut:', error);
|
|
this.showStatus(error.message, 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async updateCut(id, cutData) {
|
|
try {
|
|
const response = await fetch(`/api/cuts/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(cutData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Failed to update cut: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
this.showStatus('Cut updated successfully', 'success');
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error updating cut:', error);
|
|
this.showStatus(error.message, 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async loadCuts() {
|
|
try {
|
|
const response = await fetch('/api/cuts', {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Failed to load cuts:', response.status, errorText);
|
|
throw new Error(`Failed to load cuts: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Handle case where cuts table might not exist or be empty
|
|
if (!data.list) {
|
|
console.log('No cuts data returned, initializing empty list');
|
|
data.list = [];
|
|
}
|
|
|
|
this.allCuts = data.list || [];
|
|
this.filteredCuts = [...this.allCuts];
|
|
this.renderCutsList();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading cuts:', error);
|
|
this.showNotification('Failed to load cuts. Please check if the cuts table exists.', 'error');
|
|
// Initialize with empty array so the UI still works
|
|
this.allCuts = [];
|
|
this.filteredCuts = [];
|
|
this.renderCutsList();
|
|
}
|
|
}
|
|
|
|
renderCutsList() {
|
|
if (!this.cutsList) return;
|
|
|
|
// Remove existing event listener to prevent duplicates
|
|
this.cutsList.removeEventListener('click', this.boundHandleCutActionClick);
|
|
|
|
if (this.filteredCuts.length === 0) {
|
|
this.cutsList.innerHTML = '<p class="no-data">No cuts found</p>';
|
|
return;
|
|
}
|
|
|
|
const html = this.filteredCuts.map(cut => this.renderCutItem(cut)).join('');
|
|
this.cutsList.innerHTML = html;
|
|
|
|
// Add event delegation for cut action buttons
|
|
this.cutsList.addEventListener('click', this.boundHandleCutActionClick);
|
|
}
|
|
|
|
renderCutItem(cut) {
|
|
console.log('Rendering cut item:', cut); // Debug log to see the cut structure
|
|
const badges = [];
|
|
const isPublic = cut.is_public || cut.Is_public || cut['Public Visibility'];
|
|
const isOfficial = cut.is_official || cut.Is_official || cut['Official Cut'];
|
|
|
|
if (isPublic) badges.push('<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.showLocationManagement(cutId);
|
|
const cutName = cut.name || cut.Name || 'Unknown';
|
|
this.showStatus(`Viewing cut: ${cutName}`, 'info');
|
|
}
|
|
|
|
displayCut(cutData) {
|
|
if (this.currentCutLayer) {
|
|
this.cutsMap.removeLayer(this.currentCutLayer);
|
|
this.currentCutLayer = null;
|
|
}
|
|
|
|
if (!cutData) return false;
|
|
|
|
// Get geojson from different possible field names
|
|
const geojson = cutData.geojson || cutData.Geojson || cutData.GeoJSON || cutData['GeoJSON Data'];
|
|
if (!geojson) {
|
|
console.error('No geojson data found in cut:', cutData);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const parsedGeojson = JSON.parse(geojson);
|
|
|
|
// Get color and opacity from different possible field names
|
|
const color = cutData.color || cutData.Color || '#3388ff';
|
|
const opacity = cutData.opacity || cutData.Opacity || 0.3;
|
|
|
|
console.log('displayCut: Using color:', color, 'opacity:', opacity);
|
|
|
|
// Create GeoJSON layer with static style object (not function) for proper setStyle() support
|
|
this.currentCutLayer = L.geoJSON(parsedGeojson, {
|
|
style: {
|
|
color: color,
|
|
fillColor: color,
|
|
fillOpacity: opacity,
|
|
weight: 2,
|
|
opacity: 1.0 // Keep stroke solid
|
|
}
|
|
});
|
|
|
|
this.currentCutLayer.addTo(this.cutsMap);
|
|
|
|
// Force apply the opacity using our enhanced styling approach
|
|
this.updateLayerStyle(this.currentCutLayer, color, opacity);
|
|
|
|
console.log('displayCut: Created currentCutLayer with opacity:', opacity);
|
|
|
|
// Get bounds from different possible field names
|
|
const bounds = cutData.bounds || cutData.Bounds;
|
|
if (bounds) {
|
|
try {
|
|
const parsedBounds = JSON.parse(bounds);
|
|
this.cutsMap.fitBounds(parsedBounds, { padding: [20, 20] });
|
|
} catch (boundsError) {
|
|
this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] });
|
|
}
|
|
} else {
|
|
this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] });
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error displaying cut:', error);
|
|
this.showStatus('Failed to display cut', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async editCut(cutId) {
|
|
console.log('editCut called with ID:', cutId);
|
|
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
|
|
console.log('Found cut for editing:', cut);
|
|
if (!cut) return;
|
|
|
|
this.editingCutId = cutId;
|
|
|
|
// Use both lowercase and uppercase field names
|
|
document.getElementById('cut-name').value = cut.name || cut.Name || '';
|
|
document.getElementById('cut-description').value = cut.description || cut.Description || '';
|
|
|
|
// Set toolbar values (these are the ones we actually use now)
|
|
const cutColor = cut.color || cut.Color || '#3388ff';
|
|
const cutOpacity = cut.opacity || cut.Opacity || 0.3;
|
|
|
|
const toolbarColor = document.getElementById('toolbar-color');
|
|
const toolbarOpacity = document.getElementById('toolbar-opacity');
|
|
const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display');
|
|
|
|
if (toolbarColor) toolbarColor.value = cutColor;
|
|
if (toolbarOpacity) toolbarOpacity.value = cutOpacity;
|
|
if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = Math.round(cutOpacity * 100) + '%';
|
|
|
|
document.getElementById('cut-category').value = cut.category || cut.Category || 'Custom';
|
|
document.getElementById('cut-public').checked = cut.is_public || cut.Is_public || cut['Public Visibility'] || false;
|
|
document.getElementById('cut-official').checked = cut.is_official || cut.Is_official || cut['Official Cut'] || false;
|
|
document.getElementById('cut-geojson').value = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || '';
|
|
document.getElementById('cut-bounds').value = cut.bounds || cut.Bounds || '';
|
|
document.getElementById('cut-id').value = cut.id || cut.Id || cut.ID || cut._id;
|
|
|
|
// Store the existing geometry in form dataset
|
|
const form = document.getElementById('cut-form');
|
|
const geojsonData = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'];
|
|
const boundsData = cut.bounds || cut.Bounds;
|
|
if (form && geojsonData) {
|
|
form.dataset.geojson = geojsonData;
|
|
form.dataset.bounds = boundsData || '';
|
|
}
|
|
|
|
const cutName = cut.name || cut.Name || 'Unknown';
|
|
document.getElementById('cut-form-title').textContent = `Edit Cut: ${cutName}`;
|
|
document.getElementById('save-cut-btn').textContent = 'Update Cut';
|
|
document.getElementById('save-cut-btn').disabled = false;
|
|
document.getElementById('cancel-edit-btn').style.display = 'inline-block';
|
|
document.getElementById('start-drawing-btn').style.display = 'none';
|
|
|
|
this.updateColorValue();
|
|
this.updateOpacityValue();
|
|
this.displayCut(cut);
|
|
|
|
this.showStatus(`Editing cut: ${cutName}`, 'info');
|
|
}
|
|
|
|
async duplicateCut(cutId) {
|
|
console.log('duplicateCut called with ID:', cutId);
|
|
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
|
|
console.log('Found cut for duplication:', cut);
|
|
if (!cut) return;
|
|
|
|
// Use both lowercase and uppercase field names
|
|
const cutName = cut.name || cut.Name || 'Unknown';
|
|
const cutDescription = cut.description || cut.Description || '';
|
|
const cutColor = cut.color || cut.Color || '#3388ff';
|
|
const cutOpacity = cut.opacity || cut.Opacity || 0.3;
|
|
const cutCategory = cut.category || cut.Category || 'Custom';
|
|
const cutGeojson = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || '';
|
|
const cutBounds = cut.bounds || cut.Bounds || '';
|
|
|
|
const duplicateData = {
|
|
name: `${cutName} (Copy)`,
|
|
description: cutDescription,
|
|
color: cutColor,
|
|
opacity: cutOpacity,
|
|
category: cutCategory,
|
|
is_public: false,
|
|
is_official: false,
|
|
geojson: cutGeojson,
|
|
bounds: cutBounds
|
|
};
|
|
|
|
console.log('Duplicate data:', duplicateData);
|
|
|
|
const result = await this.createCut(duplicateData);
|
|
if (result) {
|
|
await this.loadCuts();
|
|
this.showStatus(`Duplicated cut: ${cutName}`, 'success');
|
|
}
|
|
}
|
|
|
|
async deleteCut(cutId) {
|
|
console.log('deleteCut called with ID:', cutId);
|
|
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
|
|
console.log('Found cut for deletion:', cut);
|
|
if (!cut) return;
|
|
|
|
const cutName = cut.name || cut.Name || 'Unknown';
|
|
|
|
if (!confirm(`Are you sure you want to delete the cut "${cutName}"? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/cuts/${cutId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Failed to delete cut: ${response.statusText}`);
|
|
}
|
|
|
|
this.showStatus('Cut deleted successfully', 'success');
|
|
|
|
if (this.currentCutLayer) {
|
|
this.cutsMap.removeLayer(this.currentCutLayer);
|
|
this.currentCutLayer = null;
|
|
}
|
|
|
|
await this.loadCuts();
|
|
} catch (error) {
|
|
console.error('Error deleting cut:', error);
|
|
this.showStatus(error.message, 'error');
|
|
}
|
|
}
|
|
|
|
cancelEdit() {
|
|
this.resetForm();
|
|
// Hide the cancel button
|
|
const cancelBtn = document.getElementById('cancel-edit-btn');
|
|
if (cancelBtn) {
|
|
cancelBtn.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
resetForm() {
|
|
this.form.reset();
|
|
document.getElementById('cut-id').value = '';
|
|
document.getElementById('cut-geojson').value = '';
|
|
document.getElementById('cut-bounds').value = '';
|
|
|
|
// Reset toolbar values to defaults
|
|
const toolbarColor = document.getElementById('toolbar-color');
|
|
const toolbarOpacity = document.getElementById('toolbar-opacity');
|
|
const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display');
|
|
|
|
if (toolbarColor) toolbarColor.value = '#3388ff';
|
|
if (toolbarOpacity) toolbarOpacity.value = '0.3';
|
|
if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = '30%';
|
|
|
|
// Update UI
|
|
document.getElementById('cut-form-title').textContent = 'Cut Properties';
|
|
document.getElementById('cancel-edit-btn').style.display = 'none';
|
|
|
|
// Only disable save button (form inputs stay enabled)
|
|
const saveCutBtn = document.getElementById('save-cut-btn');
|
|
if (saveCutBtn) {
|
|
saveCutBtn.disabled = true;
|
|
}
|
|
|
|
// Clear current cut
|
|
this.currentCutId = null;
|
|
|
|
// Clear any preview
|
|
if (this.cutDrawing) {
|
|
this.cutDrawing.clearPreview();
|
|
}
|
|
}
|
|
|
|
updateColorValue() {
|
|
const colorInput = document.getElementById('cut-color');
|
|
const colorValue = document.getElementById('cut-color-text');
|
|
if (colorInput && colorValue) {
|
|
colorValue.value = colorInput.value;
|
|
}
|
|
}
|
|
|
|
updateOpacityValue() {
|
|
const opacityInput = document.getElementById('cut-opacity');
|
|
const opacityValue = document.getElementById('opacity-value');
|
|
if (opacityInput && opacityValue) {
|
|
const percentage = Math.round(opacityInput.value * 100);
|
|
opacityValue.textContent = `${percentage}%`;
|
|
}
|
|
}
|
|
|
|
filterCuts() {
|
|
const searchTerm = document.getElementById('cuts-search').value.toLowerCase();
|
|
const categoryFilter = document.getElementById('cuts-category-filter').value;
|
|
|
|
let filteredCuts = this.allCuts;
|
|
|
|
if (searchTerm) {
|
|
filteredCuts = filteredCuts.filter(cut =>
|
|
cut.name.toLowerCase().includes(searchTerm) ||
|
|
(cut.description && cut.description.toLowerCase().includes(searchTerm))
|
|
);
|
|
}
|
|
|
|
if (categoryFilter) {
|
|
filteredCuts = filteredCuts.filter(cut => cut.category === categoryFilter);
|
|
}
|
|
|
|
this.filteredCuts = filteredCuts;
|
|
this.renderCutsList();
|
|
}
|
|
|
|
exportCuts() {
|
|
const exportData = {
|
|
version: '1.0',
|
|
timestamp: new Date().toISOString(),
|
|
cuts: this.allCuts.map(cut => ({
|
|
name: cut.name,
|
|
description: cut.description,
|
|
color: cut.color,
|
|
opacity: cut.opacity,
|
|
category: cut.category,
|
|
is_official: cut.is_official,
|
|
geojson: cut.geojson,
|
|
bounds: cut.bounds
|
|
}))
|
|
};
|
|
|
|
const data = JSON.stringify(exportData, null, 2);
|
|
const blob = new Blob([data], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `cuts-export-${new Date().toISOString().split('T')[0]}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.showStatus('Cuts exported successfully', 'success');
|
|
}
|
|
|
|
async handleImportFile(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const text = await file.text();
|
|
const data = JSON.parse(text);
|
|
|
|
if (!data.cuts || !Array.isArray(data.cuts)) {
|
|
throw new Error('Invalid import file format');
|
|
}
|
|
|
|
let successCount = 0;
|
|
let errorCount = 0;
|
|
|
|
for (const cutData of data.cuts) {
|
|
const result = await this.createCut(cutData);
|
|
if (result) {
|
|
successCount++;
|
|
} else {
|
|
errorCount++;
|
|
}
|
|
}
|
|
|
|
await this.loadCuts();
|
|
|
|
if (successCount > 0) {
|
|
this.showStatus(`Successfully imported ${successCount} cuts${errorCount > 0 ? `, ${errorCount} failed` : ''}`, 'success');
|
|
} else {
|
|
this.showStatus('No cuts were imported', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Import error:', error);
|
|
this.showStatus('Failed to import cuts: ' + error.message, 'error');
|
|
}
|
|
|
|
event.target.value = '';
|
|
}
|
|
|
|
// Debug method to check form state
|
|
debugFormState() {
|
|
console.log('=== Form State Debug ===');
|
|
const inputs = [
|
|
'cut-name', 'cut-description', 'cut-color',
|
|
'cut-opacity', 'cut-category', 'cut-public', 'cut-official', 'save-cut-btn'
|
|
];
|
|
|
|
inputs.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
console.log(`${id}: disabled=${element.disabled}, value="${element.value || element.checked}"`);
|
|
} else {
|
|
console.log(`${id}: NOT FOUND`);
|
|
}
|
|
});
|
|
|
|
console.log(`currentDrawingData exists: ${!!this.currentDrawingData}`);
|
|
console.log(`previewLayer exists: ${!!this.previewLayer}`);
|
|
console.log('========================');
|
|
}
|
|
|
|
// Add a debug method to check layer opacity state specifically
|
|
debugOpacityState() {
|
|
const colorElement = document.getElementById('cut-color');
|
|
const opacityElement = document.getElementById('cut-opacity');
|
|
|
|
console.log('=== DEBUG: Opacity State ===');
|
|
console.log('Color value:', colorElement?.value);
|
|
console.log('Opacity value:', opacityElement?.value);
|
|
console.log('Opacity parsed:', parseFloat(opacityElement?.value));
|
|
|
|
if (this.previewLayer) {
|
|
console.log('Preview layer exists');
|
|
this.previewLayer.eachLayer((layer) => {
|
|
console.log('Layer options fillOpacity:', layer.options.fillOpacity);
|
|
if (layer._path) {
|
|
const svgOpacity = layer._path.getAttribute('fill-opacity');
|
|
const computedStyle = window.getComputedStyle(layer._path);
|
|
console.log('SVG fill-opacity attribute:', svgOpacity);
|
|
console.log('Computed fill-opacity style:', computedStyle.fillOpacity);
|
|
console.log('SVG fill color:', layer._path.getAttribute('fill'));
|
|
}
|
|
});
|
|
} else {
|
|
console.log('No preview layer found');
|
|
}
|
|
|
|
if (this.currentCutLayer) {
|
|
console.log('Current cut layer exists');
|
|
if (this.currentCutLayer.eachLayer) {
|
|
this.currentCutLayer.eachLayer((layer) => {
|
|
console.log('Current layer options fillOpacity:', layer.options.fillOpacity);
|
|
if (layer._path) {
|
|
console.log('Current SVG fill-opacity:', layer._path.getAttribute('fill-opacity'));
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
console.log('No current cut layer found');
|
|
}
|
|
console.log('========================');
|
|
}
|
|
|
|
showStatus(message, type) {
|
|
// Use existing admin notification system if available
|
|
if (typeof showNotification === 'function') {
|
|
showNotification(message, type);
|
|
} else {
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
}
|
|
}
|
|
|
|
// Location Management Methods
|
|
setupLocationManagementControls() {
|
|
// Toggle location visibility
|
|
const toggleLocationBtn = document.getElementById('toggle-location-visibility');
|
|
if (toggleLocationBtn) {
|
|
toggleLocationBtn.addEventListener('click', () => this.toggleLocationVisibility());
|
|
}
|
|
|
|
// Export cut locations
|
|
const exportBtn = document.getElementById('export-cut-locations');
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', () => this.exportCutLocations());
|
|
}
|
|
|
|
// Print cut view
|
|
const printBtn = document.getElementById('print-cut-view');
|
|
if (printBtn) {
|
|
printBtn.addEventListener('click', () => this.printCutView());
|
|
}
|
|
|
|
// Apply filters
|
|
const applyFiltersBtn = document.getElementById('apply-filters');
|
|
if (applyFiltersBtn) {
|
|
applyFiltersBtn.addEventListener('click', () => this.applyLocationFilters());
|
|
}
|
|
|
|
// Clear filters
|
|
const clearFiltersBtn = document.getElementById('clear-filters');
|
|
if (clearFiltersBtn) {
|
|
clearFiltersBtn.addEventListener('click', () => this.clearLocationFilters());
|
|
}
|
|
|
|
// Save cut settings
|
|
const saveSettingsBtn = document.getElementById('save-cut-settings');
|
|
if (saveSettingsBtn) {
|
|
saveSettingsBtn.addEventListener('click', () => this.saveCutSettings());
|
|
}
|
|
}
|
|
|
|
async toggleLocationVisibility() {
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
const toggleBtn = document.getElementById('toggle-location-visibility');
|
|
|
|
if (this.showingLocations) {
|
|
// Hide locations
|
|
if (this.locationMarkersLayer) {
|
|
this.cutsMap.removeLayer(this.locationMarkersLayer);
|
|
}
|
|
this.showingLocations = false;
|
|
toggleBtn.textContent = 'Show Locations';
|
|
toggleBtn.classList.remove('active');
|
|
toggleBtn.classList.add('inactive');
|
|
} else {
|
|
// Show locations
|
|
await this.loadCutLocations();
|
|
toggleBtn.textContent = 'Hide Locations';
|
|
toggleBtn.classList.add('active');
|
|
toggleBtn.classList.remove('inactive');
|
|
}
|
|
}
|
|
|
|
async loadCutLocations() {
|
|
if (!this.currentCutId) return;
|
|
|
|
try {
|
|
const filters = this.getCurrentFilters();
|
|
const queryParams = new URLSearchParams(filters);
|
|
|
|
const response = await fetch(`/api/cuts/${this.currentCutId}/locations?${queryParams}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load locations: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.currentCutLocations = data.locations || [];
|
|
|
|
// Update statistics
|
|
this.updateStatistics(data.statistics);
|
|
|
|
// Display locations on map
|
|
this.displayLocationsOnMap(this.currentCutLocations);
|
|
|
|
this.showingLocations = true;
|
|
this.showStatus(`Loaded ${this.currentCutLocations.length} locations`, 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Error loading cut locations:', error);
|
|
this.showStatus('Failed to load locations', 'error');
|
|
}
|
|
}
|
|
|
|
displayLocationsOnMap(locations) {
|
|
// Remove existing markers
|
|
if (this.locationMarkersLayer) {
|
|
this.cutsMap.removeLayer(this.locationMarkersLayer);
|
|
}
|
|
|
|
// Create new markers layer
|
|
this.locationMarkersLayer = L.layerGroup();
|
|
|
|
locations.forEach(location => {
|
|
if (location.latitude && location.longitude) {
|
|
const marker = L.marker([location.latitude, location.longitude], {
|
|
icon: this.createLocationIcon(location)
|
|
});
|
|
|
|
const popupContent = this.createLocationPopup(location);
|
|
marker.bindPopup(popupContent);
|
|
|
|
this.locationMarkersLayer.addLayer(marker);
|
|
}
|
|
});
|
|
|
|
this.locationMarkersLayer.addTo(this.cutsMap);
|
|
}
|
|
|
|
createLocationIcon(location) {
|
|
// Create different icons based on support level
|
|
const supportLevel = location.support_level || location['Support Level'] || 'unknown';
|
|
const colors = {
|
|
'1': '#28a745', // Green - Strong support
|
|
'2': '#ffc107', // Yellow - Lean support
|
|
'3': '#fd7e14', // Orange - Lean opposition
|
|
'4': '#dc3545', // Red - Strong opposition
|
|
'unknown': '#6c757d' // Grey - Unknown
|
|
};
|
|
|
|
const color = colors[supportLevel] || colors['unknown'];
|
|
|
|
return L.divIcon({
|
|
className: 'location-marker',
|
|
html: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8]
|
|
});
|
|
}
|
|
|
|
createLocationPopup(location) {
|
|
// Handle different possible field names for NocoDB
|
|
const firstName = location.first_name || location['First Name'] || '';
|
|
const lastName = location.last_name || location['Last Name'] || '';
|
|
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
|
|
|
|
const address = location.address || location.Address || 'No address';
|
|
const supportLevel = location.support_level || location['Support Level'] || 'Unknown';
|
|
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
|
|
const email = location.email || location.Email || '';
|
|
const phone = location.phone || location.Phone || '';
|
|
const notes = location.notes || location.Notes || '';
|
|
|
|
return `
|
|
<div class="location-popup">
|
|
<h4>${name}</h4>
|
|
<p><strong>Address:</strong> ${address}</p>
|
|
<p><strong>Support Level:</strong> ${supportLevel}</p>
|
|
<p><strong>Lawn Sign:</strong> ${hasSign}</p>
|
|
${email ? `<p><strong>Email:</strong> ${email}</p>` : ''}
|
|
${phone ? `<p><strong>Phone:</strong> ${phone}</p>` : ''}
|
|
${notes ? `<p><strong>Notes:</strong> ${notes}</p>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getCurrentFilters() {
|
|
return {
|
|
support_level: document.getElementById('support-level-filter')?.value || '',
|
|
has_sign: document.getElementById('sign-status-filter')?.value || '',
|
|
sign_size: document.getElementById('sign-size-filter')?.value || '',
|
|
contact_filter: document.getElementById('contact-filter')?.value || ''
|
|
};
|
|
}
|
|
|
|
async applyLocationFilters() {
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
await this.loadCutLocations();
|
|
}
|
|
|
|
clearLocationFilters() {
|
|
document.getElementById('support-level-filter').value = '';
|
|
document.getElementById('sign-status-filter').value = '';
|
|
document.getElementById('sign-size-filter').value = '';
|
|
document.getElementById('contact-filter').value = '';
|
|
|
|
if (this.currentCutId && this.showingLocations) {
|
|
this.loadCutLocations();
|
|
}
|
|
}
|
|
|
|
updateStatistics(statistics) {
|
|
if (!statistics) return;
|
|
|
|
document.getElementById('total-locations').textContent = statistics.total_locations || 0;
|
|
document.getElementById('support-1').textContent = statistics.support_levels?.['1'] || 0;
|
|
document.getElementById('support-2').textContent = statistics.support_levels?.['2'] || 0;
|
|
document.getElementById('has-signs').textContent = statistics.lawn_signs?.has_sign || 0;
|
|
document.getElementById('has-email').textContent = statistics.contact_info?.has_email || 0;
|
|
document.getElementById('has-phone').textContent = statistics.contact_info?.has_phone || 0;
|
|
|
|
// Show statistics panel
|
|
document.getElementById('cut-statistics').style.display = 'block';
|
|
}
|
|
|
|
async exportCutLocations() {
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const filters = this.getCurrentFilters();
|
|
const queryParams = new URLSearchParams(filters);
|
|
|
|
const response = await fetch(`/api/cuts/${this.currentCutId}/locations/export?${queryParams}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Export failed: ${response.statusText}`);
|
|
}
|
|
|
|
// Download the CSV file
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
|
|
// Get filename from response header or use default
|
|
const contentDisposition = response.headers.get('content-disposition');
|
|
const filename = contentDisposition
|
|
? contentDisposition.split('filename=')[1]?.replace(/"/g, '')
|
|
: `cut_locations_${new Date().toISOString().split('T')[0]}.csv`;
|
|
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
this.showStatus('Locations exported successfully', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Error exporting locations:', error);
|
|
this.showStatus('Failed to export locations', 'error');
|
|
}
|
|
}
|
|
|
|
async printCutView() {
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get cut information
|
|
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.currentCutId);
|
|
const cutName = cut?.name || cut?.Name || 'Unknown Cut';
|
|
|
|
// Ensure locations are loaded before printing
|
|
if (!this.currentCutLocations || this.currentCutLocations.length === 0) {
|
|
this.showStatus('Loading locations for print...', 'info');
|
|
try {
|
|
await this.loadCutLocations();
|
|
} catch (loadError) {
|
|
console.log('Failed to load locations for print:', loadError);
|
|
this.currentCutLocations = [];
|
|
}
|
|
}
|
|
|
|
console.log('Print: Using', this.currentCutLocations.length, 'locations');
|
|
|
|
// Try multiple methods to capture the map
|
|
let mapImageDataUrl = null;
|
|
let mapInfo = '';
|
|
|
|
try {
|
|
// Method 1: Try Leaflet's built-in screenshot capability using canvas
|
|
mapImageDataUrl = await this.captureMapWithLeaflet();
|
|
|
|
if (!mapImageDataUrl) {
|
|
// Method 2: Try using DOM-to-image if available
|
|
mapImageDataUrl = await this.captureMapWithDomToImage();
|
|
}
|
|
|
|
if (!mapImageDataUrl) {
|
|
// Method 3: Try browser's built-in canvas capture
|
|
mapImageDataUrl = await this.captureMapWithCanvas();
|
|
}
|
|
|
|
if (mapImageDataUrl) {
|
|
// We successfully captured the map
|
|
mapInfo = `
|
|
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
|
|
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Map View</h3>
|
|
<img src="${mapImageDataUrl}" style="max-width: 100%; border: 1px solid #ccc; border-radius: 3px;" alt="Cut Map">
|
|
<p style="font-style: italic; color: #6c757d; margin-top: 10px; font-size: 0.9em;">
|
|
Map showing cut boundaries and ${this.currentCutLocations.length} location markers
|
|
</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// Method 4: Generate a static map visualization
|
|
const staticMap = await this.generateStaticMapImage();
|
|
if (staticMap) {
|
|
mapInfo = staticMap;
|
|
} else {
|
|
// Fallback to map information
|
|
throw new Error('All map capture methods failed');
|
|
}
|
|
}
|
|
|
|
} catch (mapError) {
|
|
console.log('Map capture failed, using fallback info:', mapError);
|
|
// Get map bounds and center for basic map info
|
|
const mapBounds = this.cutsMap.getBounds();
|
|
const center = this.cutsMap.getCenter();
|
|
const zoom = this.cutsMap.getZoom();
|
|
|
|
mapInfo = `
|
|
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
|
|
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Boundaries & Location Summary</h3>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; text-align: left;">
|
|
<div>
|
|
<p><strong>Map Center:</strong><br>${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}</p>
|
|
<p><strong>Zoom Level:</strong> ${zoom}</p>
|
|
</div>
|
|
<div>
|
|
<p><strong>Geographic Bounds:</strong><br>
|
|
North: ${mapBounds.getNorth().toFixed(6)}<br>
|
|
South: ${mapBounds.getSouth().toFixed(6)}<br>
|
|
East: ${mapBounds.getEast().toFixed(6)}<br>
|
|
West: ${mapBounds.getWest().toFixed(6)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p><strong>Cut Statistics:</strong><br>
|
|
Total Locations: ${this.currentCutLocations.length}<br>
|
|
Active Filters: ${this.getActiveFiltersCount()}<br>
|
|
Showing: ${this.currentCutLocations.length} locations
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p style="font-style: italic; color: #6c757d; margin-top: 15px; font-size: 0.9em;">
|
|
Individual location coordinates and details are listed in the table below.
|
|
</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Create print content with map info and location data
|
|
this.generatePrintContent(cutName, cut, mapInfo);
|
|
|
|
} catch (error) {
|
|
console.error('Error creating print view:', error);
|
|
this.showStatus('Failed to create print view', 'error');
|
|
|
|
// Fallback to simple print without map
|
|
this.printCutViewSimple();
|
|
}
|
|
}
|
|
|
|
async captureMapWithLeaflet() {
|
|
try {
|
|
// Simple approach: try to get the map's SVG or canvas content
|
|
const mapContainer = this.cutsMap.getContainer();
|
|
|
|
// Look for the leaflet pane that contains the actual map content
|
|
const mapPane = mapContainer.querySelector('.leaflet-map-pane');
|
|
if (!mapPane) {
|
|
console.log('Leaflet map pane not found');
|
|
return null;
|
|
}
|
|
|
|
// Try to find any canvas elements in the map
|
|
const canvases = mapPane.querySelectorAll('canvas');
|
|
if (canvases.length > 0) {
|
|
console.log('Found canvas elements, attempting to capture...');
|
|
const canvas = canvases[0];
|
|
try {
|
|
const dataUrl = canvas.toDataURL('image/png');
|
|
console.log('Successfully captured canvas');
|
|
return dataUrl;
|
|
} catch (canvasError) {
|
|
console.log('Canvas capture failed:', canvasError);
|
|
}
|
|
}
|
|
|
|
// Try SVG capture
|
|
const svgs = mapPane.querySelectorAll('svg');
|
|
if (svgs.length > 0) {
|
|
console.log('Found SVG elements, attempting to capture...');
|
|
try {
|
|
const svg = svgs[0];
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
|
|
const svgUrl = URL.createObjectURL(svgBlob);
|
|
|
|
return new Promise((resolve) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 800;
|
|
canvas.height = 600;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#f8f9fa';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
URL.revokeObjectURL(svgUrl);
|
|
console.log('Successfully captured SVG');
|
|
resolve(canvas.toDataURL('image/png'));
|
|
};
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(svgUrl);
|
|
console.log('SVG image load failed');
|
|
resolve(null);
|
|
};
|
|
img.src = svgUrl;
|
|
});
|
|
} catch (svgError) {
|
|
console.log('SVG capture failed:', svgError);
|
|
}
|
|
}
|
|
|
|
console.log('No capturable elements found in Leaflet map');
|
|
return null;
|
|
|
|
} catch (error) {
|
|
console.log('Leaflet capture failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async captureMapWithDomToImage() {
|
|
try {
|
|
// Check if dom-to-image is available
|
|
if (typeof domtoimage === 'undefined') {
|
|
console.log('dom-to-image library not available');
|
|
return null;
|
|
}
|
|
|
|
const mapElement = this.cutsMap.getContainer();
|
|
|
|
// Add some options to improve capture quality
|
|
const options = {
|
|
width: 800,
|
|
height: 600,
|
|
quality: 0.95,
|
|
bgcolor: '#f8f9fa',
|
|
style: {
|
|
transform: 'scale(1)',
|
|
transformOrigin: 'top left',
|
|
width: '800px',
|
|
height: '600px'
|
|
},
|
|
filter: (node) => {
|
|
// Skip some problematic elements
|
|
if (node.classList && node.classList.contains('leaflet-control-container')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
console.log('Attempting to capture map with dom-to-image...');
|
|
const dataUrl = await domtoimage.toPng(mapElement, options);
|
|
console.log('Map capture successful with dom-to-image');
|
|
return dataUrl;
|
|
|
|
} catch (error) {
|
|
console.log('Dom-to-image capture failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async captureMapWithCanvas() {
|
|
try {
|
|
// Try the simple canvas approach for SVG/Canvas elements
|
|
const mapElement = this.cutsMap.getContainer();
|
|
|
|
// Check if the map container has a canvas child
|
|
const canvasElements = mapElement.querySelectorAll('canvas');
|
|
if (canvasElements.length > 0) {
|
|
// Use the first canvas
|
|
const sourceCanvas = canvasElements[0];
|
|
return sourceCanvas.toDataURL('image/png');
|
|
}
|
|
|
|
// Try to capture SVG elements
|
|
const svgElements = mapElement.querySelectorAll('svg');
|
|
if (svgElements.length > 0) {
|
|
const svg = svgElements[0];
|
|
const serializer = new XMLSerializer();
|
|
const svgData = serializer.serializeToString(svg);
|
|
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
|
|
const url = URL.createObjectURL(svgBlob);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = svg.clientWidth || 800;
|
|
canvas.height = svg.clientHeight || 600;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
URL.revokeObjectURL(url);
|
|
resolve(canvas.toDataURL('image/png'));
|
|
};
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
resolve(null);
|
|
};
|
|
img.src = url;
|
|
});
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (error) {
|
|
console.log('Canvas capture failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async generateStaticMapImage() {
|
|
try {
|
|
// Create a static map representation using CSS and HTML
|
|
const mapBounds = this.cutsMap.getBounds();
|
|
const center = this.cutsMap.getCenter();
|
|
const zoom = this.cutsMap.getZoom();
|
|
|
|
// Get current locations for this cut
|
|
const cutLocations = this.currentCutLocations || [];
|
|
console.log('Static map: Using locations:', cutLocations.length);
|
|
console.log('Static map: Sample location:', cutLocations[0]);
|
|
|
|
// Calculate location positions within map bounds
|
|
const locationMarkers = cutLocations.map(location => {
|
|
const lat = parseFloat(location.latitude || location.Latitude || 0);
|
|
const lng = parseFloat(location.longitude || location.Longitude || 0);
|
|
|
|
console.log('Processing location:', location.first_name || location['First Name'], 'Coords:', lat, lng);
|
|
|
|
if (!lat || !lng) {
|
|
console.log('Invalid coordinates for location:', location);
|
|
return null;
|
|
}
|
|
|
|
// Convert to percentage positions within the map bounds
|
|
const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100;
|
|
const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100;
|
|
|
|
return {
|
|
...location,
|
|
latPercent: Math.max(2, Math.min(98, 100 - latPercent)), // Invert Y for screen coordinates
|
|
lngPercent: Math.max(2, Math.min(98, lngPercent))
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
console.log('Static map: Calculated positions for', locationMarkers.length, 'locations');
|
|
|
|
// Get the cut geometry for visualization
|
|
let cutPath = '';
|
|
let cutPolygonPoints = '';
|
|
|
|
if (this.currentCutLayer && this.currentCutLayer.getLatLngs) {
|
|
try {
|
|
const latLngs = this.currentCutLayer.getLatLngs();
|
|
const coordinates = Array.isArray(latLngs[0]) ? latLngs[0] : latLngs;
|
|
|
|
// Convert coordinates to percentages for the map visualization
|
|
const pathPoints = coordinates.map(coord => {
|
|
const lat = coord.lat;
|
|
const lng = coord.lng;
|
|
const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100;
|
|
const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100;
|
|
return `${Math.max(0, Math.min(100, lngPercent))}%,${Math.max(0, Math.min(100, 100 - latPercent))}%`;
|
|
});
|
|
|
|
cutPolygonPoints = pathPoints.join(' ');
|
|
|
|
if (pathPoints.length > 0) {
|
|
cutPath = `
|
|
<svg style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50;">
|
|
<polygon points="${cutPolygonPoints}"
|
|
fill="rgba(0, 124, 186, 0.2)"
|
|
stroke="#007cba"
|
|
stroke-width="3"
|
|
stroke-dasharray="10,5"/>
|
|
</svg>
|
|
`;
|
|
}
|
|
} catch (cutError) {
|
|
console.log('Could not extract cut geometry:', cutError);
|
|
}
|
|
}
|
|
|
|
// Create a realistic map background pattern
|
|
const mapBackground = `
|
|
background-image:
|
|
/* Street grid pattern */
|
|
linear-gradient(0deg, rgba(200,200,200,0.3) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(200,200,200,0.3) 1px, transparent 1px),
|
|
/* Neighborhood blocks */
|
|
linear-gradient(45deg, rgba(220,220,220,0.2) 25%, transparent 25%),
|
|
linear-gradient(-45deg, rgba(220,220,220,0.2) 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, rgba(220,220,220,0.2) 75%),
|
|
linear-gradient(-45deg, transparent 75%, rgba(220,220,220,0.2) 75%);
|
|
background-size:
|
|
40px 40px,
|
|
40px 40px,
|
|
80px 80px,
|
|
80px 80px,
|
|
80px 80px,
|
|
80px 80px;
|
|
background-position:
|
|
0 0,
|
|
0 0,
|
|
0 0,
|
|
0 20px,
|
|
20px -20px,
|
|
-20px 0px;
|
|
background-color: #f0f8ff;
|
|
`;
|
|
|
|
// Create a simple visual map representation
|
|
const mapVisualization = `
|
|
<div style="position: relative; width: 600px; height: 400px; border: 2px solid #ccc; margin: 0 auto; ${mapBackground}">
|
|
|
|
<!-- Cut boundary visualization -->
|
|
${cutPath}
|
|
|
|
<!-- Fallback cut boundary if SVG failed -->
|
|
${!cutPath ? '<div style="position: absolute; top: 15%; left: 15%; right: 15%; bottom: 15%; border: 3px dashed #007cba; background: rgba(0, 124, 186, 0.15); border-radius: 8px;"></div>' : ''}
|
|
|
|
<!-- Location markers -->
|
|
${locationMarkers.map((location, index) => `
|
|
<div style="position: absolute;
|
|
top: ${Math.max(2, Math.min(98, location.latPercent))}%;
|
|
left: ${Math.max(2, Math.min(98, location.lngPercent))}%;
|
|
width: 14px; height: 14px;
|
|
background: ${this.getSupportColor(location.support_level || location['Support Level'])};
|
|
border: 2px solid white;
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
|
transform: translate(-50%, -50%);
|
|
z-index: 100;
|
|
cursor: pointer;"
|
|
title="${location.first_name || location['First Name'] || ''} ${location.last_name || location['Last Name'] || ''} - ${location.address || location.Address || ''}">
|
|
<div style="position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border-radius: 50%; background: ${this.getSupportColor(location.support_level || location['Support Level'])}; opacity: 0.3; animation: pulse 2s infinite;"></div>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<!-- Map features for realism -->
|
|
<div style="position: absolute; top: 20%; left: 10%; width: 80px; height: 4px; background: rgba(100,100,100,0.6); border-radius: 2px;" title="Main Road"></div>
|
|
<div style="position: absolute; top: 60%; left: 20%; width: 60px; height: 3px; background: rgba(100,100,100,0.4); border-radius: 1px;" title="Secondary Road"></div>
|
|
<div style="position: absolute; top: 40%; left: 70%; width: 3px; height: 40px; background: rgba(100,100,100,0.4); border-radius: 1px;" title="Cross Street"></div>
|
|
|
|
<!-- Compass -->
|
|
<div style="position: absolute; top: 10px; right: 10px; width: 45px; height: 45px; background: white; border: 2px solid #333; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
|
|
<div style="color: #007cba;">N</div>
|
|
</div>
|
|
|
|
<!-- Scale indicator -->
|
|
<div style="position: absolute; bottom: 10px; left: 10px; background: rgba(255,255,255,0.9); padding: 6px 8px; border: 1px solid #333; font-size: 11px; border-radius: 3px;">
|
|
Zoom: ${zoom}<br>
|
|
Center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div style="position: absolute; bottom: 10px; right: 10px; background: rgba(255,255,255,0.95); padding: 10px; border: 1px solid #333; font-size: 11px; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
|
<div style="font-weight: bold; margin-bottom: 6px; color: #333;">Support Levels</div>
|
|
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #28a745; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Strong Support</div>
|
|
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #ffc107; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Lean Support</div>
|
|
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #fd7e14; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Oppose</div>
|
|
<div><span style="display: inline-block; width: 12px; height: 12px; background: #dc3545; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Strong Oppose</div>
|
|
</div>
|
|
|
|
<!-- Cut boundary info -->
|
|
<div style="position: absolute; top: 10px; left: 10px; background: rgba(0, 124, 186, 0.9); color: white; padding: 6px 10px; border-radius: 3px; font-size: 11px; font-weight: bold;">
|
|
Cut Boundary
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); opacity: 0.3; }
|
|
50% { transform: scale(1.4); opacity: 0.1; }
|
|
100% { transform: scale(1); opacity: 0.3; }
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
return `
|
|
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
|
|
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Map View (${locationMarkers.length} locations)</h3>
|
|
${mapVisualization}
|
|
<p style="font-style: italic; color: #6c757d; margin-top: 15px; font-size: 0.9em;">
|
|
Visual representation of cut boundaries and location positions with realistic map styling
|
|
</p>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.log('Static map generation failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getSupportColor(supportLevel) {
|
|
switch(supportLevel) {
|
|
case '1': return '#28a745'; // Strong Support - Green
|
|
case '2': return '#ffc107'; // Lean Support - Yellow
|
|
case '3': return '#fd7e14'; // Oppose - Orange
|
|
case '4': return '#dc3545'; // Strong Oppose - Red
|
|
default: return '#6c757d'; // Unknown - Gray
|
|
}
|
|
}
|
|
|
|
generatePrintContent(cutName, cut, mapContent) {
|
|
const printWindow = window.open('', '_blank');
|
|
|
|
if (!printWindow) {
|
|
this.showStatus('Popup blocked - please allow popups for print view', 'warning');
|
|
return;
|
|
}
|
|
|
|
const printContent = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Cut: ${cutName}</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 20px;
|
|
color: #333;
|
|
}
|
|
h1 {
|
|
color: #2c3e50;
|
|
border-bottom: 2px solid #3498db;
|
|
padding-bottom: 10px;
|
|
}
|
|
h2 {
|
|
color: #34495e;
|
|
margin-top: 30px;
|
|
}
|
|
.header-info {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
.stat-item {
|
|
padding: 15px;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 5px;
|
|
background: #f8f9fa;
|
|
text-align: center;
|
|
}
|
|
.stat-value {
|
|
font-size: 1.5em;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}
|
|
.stat-label {
|
|
font-size: 0.9em;
|
|
color: #6c757d;
|
|
margin-top: 5px;
|
|
}
|
|
.locations-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 20px;
|
|
font-size: 0.9em;
|
|
}
|
|
.locations-table th, .locations-table td {
|
|
border: 1px solid #dee2e6;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
.locations-table th {
|
|
background-color: #e9ecef;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
.locations-table tr:nth-child(even) {
|
|
background-color: #f8f9fa;
|
|
}
|
|
.support-level-1 { color: #28a745; font-weight: bold; }
|
|
.support-level-2 { color: #ffc107; font-weight: bold; }
|
|
.support-level-3 { color: #fd7e14; font-weight: bold; }
|
|
.support-level-3 { color: #fd7e14; font-weight: bold; }
|
|
.support-level-4 { color: #dc3545; font-weight: bold; }
|
|
.filters-info {
|
|
background: #e7f3ff;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin-bottom: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
@media print {
|
|
body { margin: 0; }
|
|
.locations-table { font-size: 0.8em; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Cut: ${cutName}</h1>
|
|
|
|
<div class="header-info">
|
|
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
|
|
<strong>Cut Category:</strong> ${cut?.category || cut?.Category || 'Unknown'}<br>
|
|
<strong>Assigned To:</strong> ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
|
|
</div>
|
|
|
|
${this.getCurrentFiltersDisplay()}
|
|
|
|
<h2>Cut Statistics</h2>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('total-locations')?.textContent || '0'}</div>
|
|
<div class="stat-label">Total Locations</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('support-1')?.textContent || '0'}</div>
|
|
<div class="stat-label">Strong Support</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('support-2')?.textContent || '0'}</div>
|
|
<div class="stat-label">Lean Support</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-signs')?.textContent || '0'}</div>
|
|
<div class="stat-label">Lawn Signs</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-email')?.textContent || '0'}</div>
|
|
<div class="stat-label">Email Contacts</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-phone')?.textContent || '0'}</div>
|
|
<div class="stat-label">Phone Contacts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Map Information</h2>
|
|
${mapContent}
|
|
|
|
<h2>Location Details</h2>
|
|
<table class="locations-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Address</th>
|
|
<th>Coordinates</th>
|
|
<th>Support Level</th>
|
|
<th>Contact Info</th>
|
|
<th>Lawn Sign</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${this.currentCutLocations.map(location => {
|
|
const firstName = location.first_name || location['First Name'] || '';
|
|
const lastName = location.last_name || location['Last Name'] || '';
|
|
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
|
|
const address = location.address || location.Address || '';
|
|
const lat = location.latitude || location.Latitude || location.lat;
|
|
const lng = location.longitude || location.Longitude || location.lng;
|
|
const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A';
|
|
const supportLevel = location.support_level || location['Support Level'] || '';
|
|
const email = location.email || location.Email || '';
|
|
const phone = location.phone || location.Phone || '';
|
|
const contact = [email, phone].filter(Boolean).join(', ');
|
|
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
|
|
const notes = location.notes || location.Notes || '';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${name}</td>
|
|
<td>${address}</td>
|
|
<td style="font-family: monospace; font-size: 0.8em;">${coordinates}</td>
|
|
<td class="support-level-${supportLevel}">${supportLevel}</td>
|
|
<td>${contact}</td>
|
|
<td>${hasSign}</td>
|
|
<td>${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 0.9em;">
|
|
<p>Generated by Campaign Map System - ${new Date().toLocaleDateString()}</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
try {
|
|
printWindow.document.write(printContent);
|
|
printWindow.document.close();
|
|
|
|
// Give the content time to render, then print
|
|
setTimeout(() => {
|
|
printWindow.print();
|
|
}, 500);
|
|
|
|
this.showStatus('Print view generated successfully', 'success');
|
|
|
|
} catch (printError) {
|
|
console.error('Error writing to print window:', printError);
|
|
printWindow.close();
|
|
this.showStatus('Print window error - using fallback', 'warning');
|
|
this.printCutViewSimple();
|
|
}
|
|
}
|
|
|
|
printCutViewSimple() {
|
|
// Simple print view without map image
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.currentCutId);
|
|
const cutName = cut?.name || cut?.Name || 'Unknown Cut';
|
|
|
|
const printWindow = window.open('', '_blank');
|
|
|
|
if (!printWindow) {
|
|
this.showStatus('Popup blocked - please allow popups for print view', 'warning');
|
|
return;
|
|
}
|
|
|
|
const printContent = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Cut: ${cutName}</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; color: #333; }
|
|
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
|
h2 { color: #34495e; margin-top: 30px; }
|
|
.header-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }
|
|
.stat-item { padding: 15px; border: 1px solid #dee2e6; border-radius: 5px; background: #f8f9fa; text-align: center; }
|
|
.stat-value { font-size: 1.5em; font-weight: bold; color: #2c3e50; }
|
|
.stat-label { font-size: 0.9em; color: #6c757d; margin-top: 5px; }
|
|
.locations-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9em; }
|
|
.locations-table th, .locations-table td { border: 1px solid #dee2e6; padding: 8px; text-align: left; }
|
|
.locations-table th { background-color: #e9ecef; font-weight: bold; color: #495057; }
|
|
.locations-table tr:nth-child(even) { background-color: #f8f9fa; }
|
|
.support-level-1 { color: #28a745; font-weight: bold; }
|
|
.support-level-2 { color: #ffc107; font-weight: bold; }
|
|
.support-level-3 { color: #fd7e14; font-weight: bold; }
|
|
.support-level-4 { color: #dc3545; font-weight: bold; }
|
|
.filters-info { background: #e7f3ff; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 0.9em; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Cut: ${cutName}</h1>
|
|
|
|
<div class="header-info">
|
|
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
|
|
<strong>Cut Category:</strong> ${cut?.category || cut?.Category || 'Unknown'}<br>
|
|
<strong>Assigned To:</strong> ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
|
|
</div>
|
|
|
|
${this.getCurrentFiltersDisplay()}
|
|
|
|
<h2>Cut Statistics</h2>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('total-locations')?.textContent || '0'}</div>
|
|
<div class="stat-label">Total Locations</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('support-1')?.textContent || '0'}</div>
|
|
<div class="stat-label">Strong Support</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('support-2')?.textContent || '0'}</div>
|
|
<div class="stat-label">Lean Support</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-signs')?.textContent || '0'}</div>
|
|
<div class="stat-label">Lawn Signs</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-email')?.textContent || '0'}</div>
|
|
<div class="stat-label">Email Contacts</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">${document.getElementById('has-phone')?.textContent || '0'}</div>
|
|
<div class="stat-label">Phone Contacts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Location Details</h2>
|
|
<table class="locations-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Address</th>
|
|
<th>Coordinates</th>
|
|
<th>Support Level</th>
|
|
<th>Contact Info</th>
|
|
<th>Lawn Sign</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${this.currentCutLocations.map(location => {
|
|
const firstName = location.first_name || location['First Name'] || '';
|
|
const lastName = location.last_name || location['Last Name'] || '';
|
|
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
|
|
const address = location.address || location.Address || '';
|
|
const lat = location.latitude || location.Latitude || location.lat;
|
|
const lng = location.longitude || location.Longitude || location.lng;
|
|
const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A';
|
|
const supportLevel = location.support_level || location['Support Level'] || '';
|
|
const email = location.email || location.Email || '';
|
|
const phone = location.phone || location.Phone || '';
|
|
const contact = [email, phone].filter(Boolean).join(', ');
|
|
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
|
|
const notes = location.notes || location.Notes || '';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${name}</td>
|
|
<td>${address}</td>
|
|
<td style="font-family: monospace; font-size: 0.8em;">${coordinates}</td>
|
|
<td class="support-level-${supportLevel}">${supportLevel}</td>
|
|
<td>${contact}</td>
|
|
<td>${hasSign}</td>
|
|
<td>${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 0.9em;">
|
|
<p>Generated by Campaign Map System - ${new Date().toLocaleDateString()}</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
try {
|
|
if (!printWindow || printWindow.closed) {
|
|
this.showStatus('Print window unavailable', 'error');
|
|
return;
|
|
}
|
|
|
|
printWindow.document.write(printContent);
|
|
printWindow.document.close();
|
|
|
|
// Give content time to load then print
|
|
setTimeout(() => {
|
|
if (!printWindow.closed) {
|
|
printWindow.print();
|
|
}
|
|
}, 300);
|
|
|
|
this.showStatus('Simple print view generated', 'success');
|
|
|
|
} catch (printError) {
|
|
console.error('Error in simple print:', printError);
|
|
if (printWindow && !printWindow.closed) {
|
|
printWindow.close();
|
|
}
|
|
this.showStatus('Print failed - please try again', 'error');
|
|
}
|
|
}
|
|
|
|
getCurrentFiltersDisplay() {
|
|
const activeFilters = [];
|
|
|
|
// Check location filters
|
|
const supportFilter = document.getElementById('support-filter')?.value;
|
|
if (supportFilter && supportFilter !== 'all') {
|
|
activeFilters.push(`Support Level: ${supportFilter}`);
|
|
}
|
|
|
|
const signFilter = document.getElementById('sign-filter')?.value;
|
|
if (signFilter && signFilter !== 'all') {
|
|
activeFilters.push(`Lawn Signs: ${signFilter === 'true' ? 'Yes' : 'No'}`);
|
|
}
|
|
|
|
const contactFilter = document.getElementById('contact-filter')?.value;
|
|
if (contactFilter && contactFilter !== 'all') {
|
|
const contactLabels = {
|
|
'email': 'Has Email',
|
|
'phone': 'Has Phone',
|
|
'both': 'Has Both Email & Phone',
|
|
'none': 'No Contact Info'
|
|
};
|
|
activeFilters.push(`Contact: ${contactLabels[contactFilter] || contactFilter}`);
|
|
}
|
|
|
|
if (activeFilters.length === 0) {
|
|
return '<div class="filters-info"><strong>Filters:</strong> None (showing all locations)</div>';
|
|
}
|
|
|
|
return `<div class="filters-info"><strong>Active Filters:</strong> ${activeFilters.join(', ')}</div>`;
|
|
}
|
|
|
|
getActiveFiltersCount() {
|
|
let count = 0;
|
|
|
|
const supportFilter = document.getElementById('support-filter')?.value;
|
|
if (supportFilter && supportFilter !== 'all') count++;
|
|
|
|
const signFilter = document.getElementById('sign-filter')?.value;
|
|
if (signFilter && signFilter !== 'all') count++;
|
|
|
|
const contactFilter = document.getElementById('contact-filter')?.value;
|
|
if (contactFilter && contactFilter !== 'all') count++;
|
|
|
|
return count;
|
|
}
|
|
|
|
async saveCutSettings() {
|
|
if (!this.currentCutId) {
|
|
this.showStatus('No cut selected', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const settings = {
|
|
show_locations: document.getElementById('show-locations-toggle').checked,
|
|
export_enabled: document.getElementById('export-enabled-toggle').checked,
|
|
assigned_to: document.getElementById('assigned-to').value,
|
|
completion_percentage: parseInt(document.getElementById('completion-percentage').value) || 0
|
|
};
|
|
|
|
const response = await fetch(`/api/cuts/${this.currentCutId}/settings`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(settings)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to save settings: ${response.statusText}`);
|
|
}
|
|
|
|
this.showStatus('Cut settings saved successfully', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Error saving cut settings:', error);
|
|
this.showStatus('Failed to save cut settings', 'error');
|
|
}
|
|
}
|
|
|
|
showLocationManagement(cutId) {
|
|
console.log('showLocationManagement called with cutId:', cutId);
|
|
this.currentCutId = cutId;
|
|
const locationSection = document.getElementById('cut-location-management');
|
|
console.log('Location section element:', locationSection);
|
|
|
|
if (locationSection) {
|
|
console.log('Setting location section display to block');
|
|
locationSection.style.display = 'block';
|
|
|
|
// Load cut data to populate settings - use multiple possible ID field names for NocoDB
|
|
const cut = this.allCuts.find(c =>
|
|
(c.id || c.Id || c.ID || c._id) == cutId
|
|
);
|
|
console.log('Found cut for location management:', cut);
|
|
console.log('Available cuts:', this.allCuts.map(c => ({
|
|
id: c.id || c.Id || c.ID || c._id,
|
|
name: c.name || c.Name
|
|
})));
|
|
|
|
if (cut) {
|
|
const toggleElement = document.getElementById('show-locations-toggle');
|
|
const exportElement = document.getElementById('export-enabled-toggle');
|
|
const assignedElement = document.getElementById('assigned-to');
|
|
const completionElement = document.getElementById('completion-percentage');
|
|
|
|
console.log('Setting up toggles:', {
|
|
toggleElement: !!toggleElement,
|
|
exportElement: !!exportElement,
|
|
assignedElement: !!assignedElement,
|
|
completionElement: !!completionElement
|
|
});
|
|
|
|
if (toggleElement) toggleElement.checked = (cut.show_locations || cut.Show_locations || cut['Show Locations']) !== false;
|
|
if (exportElement) exportElement.checked = (cut.export_enabled || cut.Export_enabled || cut['Export Enabled']) !== false;
|
|
if (assignedElement) assignedElement.value = cut.assigned_to || cut.Assigned_to || cut['Assigned To'] || '';
|
|
if (completionElement) completionElement.value = cut.completion_percentage || cut.Completion_percentage || cut['Completion Percentage'] || '';
|
|
}
|
|
} else {
|
|
console.error('Location management section not found!');
|
|
}
|
|
}
|
|
|
|
hideLocationManagement() {
|
|
const locationSection = document.getElementById('cut-location-management');
|
|
if (locationSection) {
|
|
locationSection.style.display = 'none';
|
|
}
|
|
|
|
// Clear current data
|
|
this.currentCutId = null;
|
|
this.currentCutLocations = [];
|
|
|
|
// Hide locations on map
|
|
if (this.locationMarkersLayer) {
|
|
this.cutsMap.removeLayer(this.locationMarkersLayer);
|
|
}
|
|
this.showingLocations = false;
|
|
|
|
// Reset button state
|
|
const toggleBtn = document.getElementById('toggle-location-visibility');
|
|
if (toggleBtn) {
|
|
toggleBtn.textContent = 'Show Locations';
|
|
toggleBtn.classList.remove('active');
|
|
toggleBtn.classList.remove('inactive');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global instance
|
|
const adminCutsManager = new AdminCutsManager();
|
|
window.adminCutsManager = adminCutsManager;
|
|
|
|
// Expose debug function globally for easy access
|
|
window.debugCutsForm = () => adminCutsManager.debugFormState();
|
|
window.debugOpacity = () => adminCutsManager.debugOpacityState();
|
|
window.forceUpdateCutStyle = () => adminCutsManager.forceUpdateDrawingStyle();
|
|
window.syncToolbarValues = () => adminCutsManager.syncToolbarDisplayValues();
|
|
|
|
// Initialize when cuts section becomes visible
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize immediately if cuts section is already visible
|
|
const cutsSection = document.getElementById('cuts');
|
|
if (cutsSection && cutsSection.style.display !== 'none') {
|
|
adminCutsManager.initialize().catch(error => {
|
|
console.error('Failed to initialize cuts manager:', error);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Otherwise, watch for it to become visible
|
|
const observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.type === 'attributes' &&
|
|
mutation.attributeName === 'style' &&
|
|
mutation.target.id === 'cuts' &&
|
|
mutation.target.style.display !== 'none' &&
|
|
!adminCutsManager.isInitialized) {
|
|
|
|
// Small delay to ensure DOM is fully rendered
|
|
setTimeout(() => {
|
|
adminCutsManager.initialize().catch(error => {
|
|
console.error('Failed to initialize cuts manager:', error);
|
|
});
|
|
}, 100);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (cutsSection) {
|
|
observer.observe(cutsSection, { attributes: true });
|
|
}
|
|
});
|