Fixed the print view and things seem to be working now.
This commit is contained in:
parent
b3cd1a3331
commit
459cea0c3b
@ -1377,7 +1377,11 @@
|
||||
<!-- Dashboard JavaScript -->
|
||||
<script src="js/dashboard.js"></script>
|
||||
|
||||
<!-- Admin Cuts JavaScript -->
|
||||
<!-- Admin Cuts JavaScript - Load modules first, then main file -->
|
||||
<script src="js/cut-drawing.js"></script>
|
||||
<script src="js/cut-location-manager.js"></script>
|
||||
<script src="js/cut-print-utils.js"></script>
|
||||
<script src="js/admin-cuts-manager.js"></script>
|
||||
<script src="js/admin-cuts.js"></script>
|
||||
|
||||
<!-- Listmonk Status and Admin -->
|
||||
|
||||
@ -16,3 +16,4 @@
|
||||
|
||||
/* Legacy import compatibility */
|
||||
@import url("modules/dashboard.css");
|
||||
@import url("modules/cuts.css");
|
||||
|
||||
@ -209,7 +209,11 @@ function loadSectionData(sectionId) {
|
||||
console.error('Failed to initialize cuts manager:', error);
|
||||
});
|
||||
} else {
|
||||
console.log('Admin cuts manager already initialized');
|
||||
console.log('Admin cuts manager already initialized, refreshing map...');
|
||||
// Refresh map size when section becomes visible
|
||||
if (window.adminCutsManager.refreshMapSize) {
|
||||
window.adminCutsManager.refreshMapSize();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('adminCutsManager not found in showSection');
|
||||
|
||||
110
map/app/public/js/admin-cuts-main.js
Normal file
110
map/app/public/js/admin-cuts-main.js
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Admin Cuts Management Module
|
||||
* Main initialization file that imports and orchestrates all cut management modules
|
||||
*/
|
||||
|
||||
// Global admin cuts manager instance
|
||||
let adminCutsManager = null;
|
||||
|
||||
// Initialize the admin cuts system when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
console.log('DOM loaded, initializing admin cuts system...');
|
||||
|
||||
try {
|
||||
// Wait for all module classes to be loaded
|
||||
if (typeof AdminCutsManager === 'undefined') {
|
||||
console.error('AdminCutsManager class not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof CutDrawing === 'undefined') {
|
||||
console.error('CutDrawing class not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof CutLocationManager === 'undefined') {
|
||||
console.error('CutLocationManager class not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof CutPrintUtils === 'undefined') {
|
||||
console.error('CutPrintUtils class not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create manager instance
|
||||
adminCutsManager = new AdminCutsManager();
|
||||
|
||||
// Initialize the system
|
||||
await adminCutsManager.initialize();
|
||||
|
||||
console.log('Admin cuts system initialized successfully');
|
||||
|
||||
// Make manager globally accessible for debugging
|
||||
window.adminCutsManager = adminCutsManager;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize admin cuts system:', error);
|
||||
|
||||
// Show error notification if available
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Failed to initialize cuts management system', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Global functions for backward compatibility
|
||||
function startDrawing() {
|
||||
if (adminCutsManager && adminCutsManager.cutDrawing) {
|
||||
adminCutsManager.handleStartDrawing();
|
||||
}
|
||||
}
|
||||
|
||||
function finishDrawing() {
|
||||
if (adminCutsManager && adminCutsManager.cutDrawing) {
|
||||
adminCutsManager.cutDrawing.finishDrawing();
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDrawing() {
|
||||
if (adminCutsManager && adminCutsManager.cutDrawing) {
|
||||
adminCutsManager.cutDrawing.cancelDrawing();
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
function exportCuts() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.exportCuts();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCuts() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.loadCuts();
|
||||
}
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
function debugFormState() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.debugFormState();
|
||||
}
|
||||
}
|
||||
|
||||
function debugOpacityState() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.debugOpacityState();
|
||||
}
|
||||
}
|
||||
|
||||
function forceUpdateDrawingStyle() {
|
||||
if (adminCutsManager) {
|
||||
adminCutsManager.forceUpdateDrawingStyle();
|
||||
}
|
||||
}
|
||||
1575
map/app/public/js/admin-cuts-manager.js
Normal file
1575
map/app/public/js/admin-cuts-manager.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
425
map/app/public/js/cut-drawing-new.js
Normal file
425
map/app/public/js/cut-drawing-new.js
Normal file
@ -0,0 +1,425 @@
|
||||
/**
|
||||
* Cut Drawing Module
|
||||
* Handles polygon drawing functionality for cut creation
|
||||
*/
|
||||
|
||||
class CutDrawing {
|
||||
constructor(map) {
|
||||
this.map = map;
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
this.polyline = null;
|
||||
this.polygon = null; // Add polygon preview
|
||||
this.finalPolygon = null; // Final polygon after finishing
|
||||
this.isDrawing = false;
|
||||
this.onFinishCallback = null;
|
||||
this.onCancelCallback = null;
|
||||
this.currentColor = '#3388ff';
|
||||
this.currentOpacity = 0.3;
|
||||
}
|
||||
|
||||
startDrawing(onFinish, onCancel) {
|
||||
if (this.isDrawing) {
|
||||
this.cancelDrawing();
|
||||
}
|
||||
|
||||
this.isDrawing = true;
|
||||
this.onFinishCallback = onFinish;
|
||||
this.onCancelCallback = onCancel;
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
|
||||
this.map.getContainer().style.cursor = 'crosshair';
|
||||
this.map.on('click', this.onMapClick.bind(this));
|
||||
this.map.doubleClickZoom.disable();
|
||||
|
||||
console.log('Cut drawing started - click to add points');
|
||||
}
|
||||
|
||||
onMapClick(e) {
|
||||
if (!this.isDrawing) return;
|
||||
|
||||
// Check if clicking on first vertex to close polygon
|
||||
if (this.vertices.length >= 3) {
|
||||
const firstVertex = this.vertices[0];
|
||||
const clickPoint = this.map.latLngToContainerPoint(e.latlng);
|
||||
const firstPoint = this.map.latLngToContainerPoint(firstVertex);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(clickPoint.x - firstPoint.x, 2) +
|
||||
Math.pow(clickPoint.y - firstPoint.y, 2)
|
||||
);
|
||||
|
||||
// If clicked within 15 pixels of first vertex, close the polygon
|
||||
if (distance < 15) {
|
||||
console.log('Closing polygon by clicking first vertex');
|
||||
this.finishDrawing();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add vertex marker with special styling for first vertex
|
||||
const isFirstVertex = this.vertices.length === 0;
|
||||
const marker = L.marker(e.latlng, {
|
||||
icon: L.divIcon({
|
||||
className: 'cut-vertex-marker' + (isFirstVertex ? ' first-vertex' : ''),
|
||||
html: `<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class if using modules, otherwise it's global
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CutDrawing;
|
||||
} else {
|
||||
window.CutDrawing = CutDrawing;
|
||||
}
|
||||
@ -1,22 +1,23 @@
|
||||
/**
|
||||
* Cut Drawing Module
|
||||
* Handles polygon drawing functionality for creating map cuts
|
||||
* Handles polygon drawing functionality for cut creation
|
||||
*/
|
||||
|
||||
export class CutDrawing {
|
||||
constructor(map, options = {}) {
|
||||
class CutDrawing {
|
||||
constructor(map) {
|
||||
this.map = map;
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
this.polyline = null;
|
||||
this.previewPolygon = null; // Add preview polygon
|
||||
this.polygon = null; // Add polygon preview
|
||||
this.finalPolygon = null; // Final polygon after finishing
|
||||
this.isDrawing = false;
|
||||
this.onComplete = options.onComplete || null;
|
||||
this.onFinishCallback = null;
|
||||
this.onCancelCallback = null;
|
||||
this.currentColor = '#3388ff';
|
||||
this.currentOpacity = 0.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start drawing mode
|
||||
*/
|
||||
startDrawing(onFinish, onCancel) {
|
||||
if (this.isDrawing) {
|
||||
this.cancelDrawing();
|
||||
@ -28,39 +29,64 @@ export class CutDrawing {
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
|
||||
// Change cursor and add click listener
|
||||
this.map.getContainer().style.cursor = 'crosshair';
|
||||
this.map.on('click', this.onMapClick.bind(this));
|
||||
|
||||
// Disable double-click zoom while drawing
|
||||
this.map.doubleClickZoom.disable();
|
||||
|
||||
console.log('Cut drawing started - click to add points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle map clicks to add vertices
|
||||
*/
|
||||
onMapClick(e) {
|
||||
if (!this.isDrawing) return;
|
||||
|
||||
// Add vertex marker
|
||||
// Check if clicking on first vertex to close polygon
|
||||
if (this.vertices.length >= 3) {
|
||||
const firstVertex = this.vertices[0];
|
||||
const clickPoint = this.map.latLngToContainerPoint(e.latlng);
|
||||
const firstPoint = this.map.latLngToContainerPoint(firstVertex);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(clickPoint.x - firstPoint.x, 2) +
|
||||
Math.pow(clickPoint.y - firstPoint.y, 2)
|
||||
);
|
||||
|
||||
// If clicked within 15 pixels of first vertex, close the polygon
|
||||
if (distance < 15) {
|
||||
console.log('Closing polygon by clicking first vertex');
|
||||
this.finishDrawing();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add vertex marker with special styling for first vertex
|
||||
const isFirstVertex = this.vertices.length === 0;
|
||||
const marker = L.marker(e.latlng, {
|
||||
icon: L.divIcon({
|
||||
className: 'cut-vertex-marker',
|
||||
html: '<div class="vertex-point"></div>',
|
||||
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);
|
||||
|
||||
// Update the polyline
|
||||
this.updatePolyline();
|
||||
|
||||
// Show tooltip on first vertex when we have enough points
|
||||
if (this.vertices.length === 3 && this.markers[0]) {
|
||||
this.markers[0].openTooltip();
|
||||
}
|
||||
|
||||
// Call update callback if available
|
||||
if (this.onUpdate) {
|
||||
this.onUpdate();
|
||||
@ -69,81 +95,126 @@ export class CutDrawing {
|
||||
console.log(`Added vertex ${this.vertices.length} at`, e.latlng);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the polyline connecting vertices
|
||||
*/
|
||||
updatePolyline() {
|
||||
// Remove existing polyline
|
||||
// Use stored color and opacity values
|
||||
const color = this.currentColor;
|
||||
const opacity = this.currentOpacity;
|
||||
|
||||
if (this.polyline) {
|
||||
this.map.removeLayer(this.polyline);
|
||||
}
|
||||
|
||||
if (this.polygon) {
|
||||
this.map.removeLayer(this.polygon);
|
||||
this.polygon = null;
|
||||
}
|
||||
|
||||
if (this.vertices.length > 1) {
|
||||
// Create polyline connecting all vertices
|
||||
// Show polyline for incomplete polygon
|
||||
this.polyline = L.polyline(this.vertices, {
|
||||
color: '#3388ff',
|
||||
color: color,
|
||||
weight: 2,
|
||||
dashArray: '5, 5',
|
||||
opacity: 0.8
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish drawing and create polygon
|
||||
*/
|
||||
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');
|
||||
return;
|
||||
alert('A cut must have at least 3 points. Click more points or cancel drawing.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the polygon
|
||||
const latlngs = this.vertices.map(v => v.getLatLng());
|
||||
// Store vertices before cleanup
|
||||
const finalVertices = [...this.vertices];
|
||||
|
||||
// Close the polygon
|
||||
latlngs.push(latlngs[0]);
|
||||
// Use stored color and opacity values
|
||||
const color = this.currentColor;
|
||||
const opacity = this.currentOpacity;
|
||||
|
||||
// Generate GeoJSON
|
||||
const geojson = {
|
||||
type: 'Polygon',
|
||||
coordinates: [latlngs.map(ll => [ll.lng, ll.lat])]
|
||||
// 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
|
||||
};
|
||||
|
||||
// Calculate bounds
|
||||
const bounds = {
|
||||
north: Math.max(...latlngs.map(ll => ll.lat)),
|
||||
south: Math.min(...latlngs.map(ll => ll.lat)),
|
||||
east: Math.max(...latlngs.map(ll => ll.lng)),
|
||||
west: Math.min(...latlngs.map(ll => ll.lng))
|
||||
};
|
||||
|
||||
console.log('Cut drawing finished with', this.vertices.length, 'vertices');
|
||||
|
||||
// Show preview before clearing drawing
|
||||
const color = document.getElementById('cut-color')?.value || '#3388ff';
|
||||
const opacity = parseFloat(document.getElementById('cut-opacity')?.value) || 0.3;
|
||||
this.showPreview(geojson, color, opacity);
|
||||
|
||||
// Clean up drawing elements
|
||||
this.clearDrawing();
|
||||
|
||||
// Call completion callback with the data
|
||||
if (this.onComplete && typeof this.onComplete === 'function') {
|
||||
console.log('Calling completion callback with geojson and bounds');
|
||||
this.onComplete(geojson, bounds);
|
||||
if (this.onFinishCallback) {
|
||||
console.log('Calling onFinishCallback');
|
||||
this.onFinishCallback(callbackData);
|
||||
} else {
|
||||
console.error('No completion callback defined');
|
||||
console.log('No onFinishCallback set');
|
||||
}
|
||||
|
||||
// Reset state
|
||||
this.isDrawing = false;
|
||||
this.updateToolbar();
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel drawing
|
||||
*/
|
||||
cancelDrawing() {
|
||||
if (!this.isDrawing) return;
|
||||
|
||||
@ -153,22 +224,22 @@ export class CutDrawing {
|
||||
if (this.onCancelCallback) {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
|
||||
// Also call the onCancel callback if set
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the last added vertex
|
||||
*/
|
||||
undoLastVertex() {
|
||||
if (!this.isDrawing || this.vertices.length === 0) return;
|
||||
|
||||
// Remove last vertex and marker
|
||||
this.vertices.pop();
|
||||
const lastMarker = this.markers.pop();
|
||||
if (lastMarker) {
|
||||
this.map.removeLayer(lastMarker);
|
||||
}
|
||||
|
||||
// Update polyline
|
||||
this.updatePolyline();
|
||||
|
||||
// Call update callback if available
|
||||
@ -179,24 +250,23 @@ export class CutDrawing {
|
||||
console.log('Removed last vertex, remaining:', this.vertices.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all vertices and start over
|
||||
*/
|
||||
clearVertices() {
|
||||
if (!this.isDrawing) return;
|
||||
|
||||
// Remove all markers
|
||||
this.markers.forEach(marker => {
|
||||
this.map.removeLayer(marker);
|
||||
});
|
||||
|
||||
// Remove polyline
|
||||
if (this.polyline) {
|
||||
this.map.removeLayer(this.polyline);
|
||||
this.polyline = null;
|
||||
}
|
||||
|
||||
// Reset arrays
|
||||
if (this.polygon) {
|
||||
this.map.removeLayer(this.polygon);
|
||||
this.polygon = null;
|
||||
}
|
||||
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
|
||||
@ -208,41 +278,64 @@ export class CutDrawing {
|
||||
console.log('Cleared all vertices');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup drawing state
|
||||
*/
|
||||
cleanup() {
|
||||
// Remove all markers
|
||||
cleanupDrawingElements() {
|
||||
// Remove drawing elements but preserve final polygon
|
||||
this.markers.forEach(marker => {
|
||||
this.map.removeLayer(marker);
|
||||
});
|
||||
|
||||
// Remove polyline
|
||||
if (this.polyline) {
|
||||
this.map.removeLayer(this.polyline);
|
||||
}
|
||||
|
||||
// Reset cursor
|
||||
if (this.polygon) {
|
||||
this.map.removeLayer(this.polygon);
|
||||
}
|
||||
|
||||
this.map.getContainer().style.cursor = '';
|
||||
|
||||
// Remove event listeners
|
||||
this.map.off('click', this.onMapClick);
|
||||
|
||||
// Re-enable double-click zoom
|
||||
this.map.doubleClickZoom.enable();
|
||||
|
||||
// Reset state
|
||||
this.isDrawing = false;
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
this.polyline = null;
|
||||
this.polygon = null;
|
||||
this.onFinishCallback = null;
|
||||
this.onCancelCallback = null;
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.markers.forEach(marker => {
|
||||
this.map.removeLayer(marker);
|
||||
});
|
||||
|
||||
if (this.polyline) {
|
||||
this.map.removeLayer(this.polyline);
|
||||
}
|
||||
|
||||
if (this.polygon) {
|
||||
this.map.removeLayer(this.polygon);
|
||||
}
|
||||
|
||||
if (this.finalPolygon) {
|
||||
this.map.removeLayer(this.finalPolygon);
|
||||
}
|
||||
|
||||
this.map.getContainer().style.cursor = '';
|
||||
this.map.off('click', this.onMapClick);
|
||||
this.map.doubleClickZoom.enable();
|
||||
|
||||
this.isDrawing = false;
|
||||
this.vertices = [];
|
||||
this.markers = [];
|
||||
this.polyline = null;
|
||||
this.polygon = null;
|
||||
this.finalPolygon = null;
|
||||
this.onFinishCallback = null;
|
||||
this.onCancelCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current drawing state
|
||||
*/
|
||||
getState() {
|
||||
return {
|
||||
isDrawing: this.isDrawing,
|
||||
@ -251,86 +344,82 @@ export class CutDrawing {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview polygon without finishing
|
||||
*/
|
||||
showPreview(geojson, color = '#3388ff', opacity = 0.3) {
|
||||
this.clearPreview();
|
||||
// Add method to update current drawing style
|
||||
updateDrawingStyle(color, opacity) {
|
||||
this.currentColor = color;
|
||||
this.currentOpacity = opacity;
|
||||
|
||||
if (!geojson) return;
|
||||
console.log(`CutDrawing.updateDrawingStyle called with color: ${color}, opacity: ${opacity}`);
|
||||
|
||||
try {
|
||||
const coordinates = geojson.coordinates[0];
|
||||
const latlngs = coordinates.map(coord => L.latLng(coord[1], coord[0]));
|
||||
// Update polyline color
|
||||
if (this.polyline) {
|
||||
this.polyline.setStyle({ color: color });
|
||||
}
|
||||
|
||||
this.previewPolygon = L.polygon(latlngs, {
|
||||
// Update polygon if it exists
|
||||
if (this.polygon) {
|
||||
this.polygon.setStyle({
|
||||
color: color,
|
||||
weight: 2,
|
||||
opacity: 0.8,
|
||||
fillColor: color,
|
||||
fillOpacity: opacity,
|
||||
className: 'cut-preview-polygon'
|
||||
}).addTo(this.map);
|
||||
|
||||
// Add CSS class for opacity control
|
||||
const pathElement = this.previewPolygon.getElement();
|
||||
if (pathElement) {
|
||||
pathElement.classList.add('cut-polygon');
|
||||
console.log('Added cut-polygon class to preview polygon');
|
||||
}
|
||||
|
||||
console.log('Preview polygon shown with opacity:', opacity);
|
||||
} catch (error) {
|
||||
console.error('Error showing preview polygon:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview polygon style without recreating it
|
||||
*/
|
||||
updatePreview(color = '#3388ff', opacity = 0.3) {
|
||||
if (this.previewPolygon) {
|
||||
this.previewPolygon.setStyle({
|
||||
color: color,
|
||||
weight: 2,
|
||||
opacity: 0.8,
|
||||
fillColor: color,
|
||||
fillOpacity: opacity
|
||||
opacity: 0.8 // Border opacity
|
||||
});
|
||||
|
||||
// Ensure CSS class is still present
|
||||
const pathElement = this.previewPolygon.getElement();
|
||||
if (pathElement) {
|
||||
pathElement.classList.add('cut-polygon');
|
||||
// Force inline style update and browser reflow
|
||||
if (this.polygon._path) {
|
||||
// Use setProperty with important flag for stronger override
|
||||
this.polygon._path.style.setProperty('fill-opacity', opacity, 'important');
|
||||
this.polygon._path.style.setProperty('fill', color, 'important');
|
||||
|
||||
// Force multiple reflows to ensure update
|
||||
void this.polygon._path.offsetHeight;
|
||||
void this.polygon._path.offsetWidth;
|
||||
|
||||
// Force repaint by temporarily changing a property
|
||||
const oldDisplay = this.polygon._path.style.display;
|
||||
this.polygon._path.style.display = 'none';
|
||||
void this.polygon._path.offsetHeight;
|
||||
this.polygon._path.style.display = oldDisplay;
|
||||
}
|
||||
|
||||
console.log('Preview polygon style updated with opacity:', opacity);
|
||||
// Also try Leaflet's internal redraw
|
||||
if (this.polygon._updatePath) {
|
||||
this.polygon._updatePath();
|
||||
}
|
||||
|
||||
console.log(`Updated active drawing polygon with opacity: ${opacity}`);
|
||||
} else {
|
||||
// If no polygon exists but we have vertices, force a complete redraw
|
||||
console.log('No polygon exists, forcing complete redraw');
|
||||
this.forceRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
clearPreview() {
|
||||
if (this.previewPolygon) {
|
||||
this.map.removeLayer(this.previewPolygon);
|
||||
this.previewPolygon = null;
|
||||
// Clear any preview polygons but not the final polygon
|
||||
if (this.polygon) {
|
||||
this.map.removeLayer(this.polygon);
|
||||
this.polygon = null;
|
||||
}
|
||||
|
||||
if (this.polyline && this.isDrawing) {
|
||||
this.map.removeLayer(this.polyline);
|
||||
this.polyline = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drawing style (called from admin cuts manager)
|
||||
*/
|
||||
updateDrawingStyle(color = '#3388ff', opacity = 0.3) {
|
||||
// Update the polyline connecting vertices if it exists
|
||||
if (this.polyline) {
|
||||
this.polyline.setStyle({
|
||||
color: color,
|
||||
weight: 2,
|
||||
opacity: 0.8
|
||||
});
|
||||
// Force a complete redraw with current style settings
|
||||
forceRedraw() {
|
||||
if (this.vertices.length > 1) {
|
||||
console.log('Forcing complete redraw with vertices:', this.vertices.length);
|
||||
this.updatePolyline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview polygon if it exists
|
||||
this.updatePreview(color, opacity);
|
||||
|
||||
console.log('Cut drawing style updated with color:', color, 'opacity:', opacity);
|
||||
}
|
||||
// Export the class if using modules, otherwise it's global
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CutDrawing;
|
||||
} else {
|
||||
window.CutDrawing = CutDrawing;
|
||||
}
|
||||
|
||||
443
map/app/public/js/cut-location-manager.js
Normal file
443
map/app/public/js/cut-location-manager.js
Normal file
@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Cut Location Management Module
|
||||
* Handles location display, filtering, statistics, and management for cuts
|
||||
*/
|
||||
|
||||
class CutLocationManager {
|
||||
constructor(map, cutsManager) {
|
||||
this.map = map;
|
||||
this.cutsManager = cutsManager;
|
||||
this.currentCutId = null;
|
||||
this.currentCutLocations = [];
|
||||
this.currentFilters = {};
|
||||
this.locationMarkersLayer = null;
|
||||
this.showingLocations = false;
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// Toggle location visibility
|
||||
const toggleLocationBtn = document.getElementById('toggle-location-visibility');
|
||||
if (toggleLocationBtn) {
|
||||
toggleLocationBtn.addEventListener('click', () => this.toggleLocationVisibility());
|
||||
}
|
||||
|
||||
// Export cut locations
|
||||
const exportBtn = document.getElementById('export-cut-locations');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', () => this.exportCutLocations());
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
const applyFiltersBtn = document.getElementById('apply-filters');
|
||||
if (applyFiltersBtn) {
|
||||
applyFiltersBtn.addEventListener('click', () => this.applyLocationFilters());
|
||||
}
|
||||
|
||||
// Clear filters
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
if (clearFiltersBtn) {
|
||||
clearFiltersBtn.addEventListener('click', () => this.clearLocationFilters());
|
||||
}
|
||||
|
||||
// Save cut settings
|
||||
const saveSettingsBtn = document.getElementById('save-cut-settings');
|
||||
if (saveSettingsBtn) {
|
||||
saveSettingsBtn.addEventListener('click', () => this.saveCutSettings());
|
||||
}
|
||||
}
|
||||
|
||||
async toggleLocationVisibility() {
|
||||
if (!this.currentCutId) {
|
||||
this.showStatus('No cut selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleBtn = document.getElementById('toggle-location-visibility');
|
||||
|
||||
if (this.showingLocations) {
|
||||
// Hide locations
|
||||
if (this.locationMarkersLayer) {
|
||||
this.map.removeLayer(this.locationMarkersLayer);
|
||||
}
|
||||
this.showingLocations = false;
|
||||
toggleBtn.textContent = 'Show Locations';
|
||||
toggleBtn.classList.remove('active');
|
||||
toggleBtn.classList.add('inactive');
|
||||
} else {
|
||||
// Show locations
|
||||
await this.loadCutLocations();
|
||||
toggleBtn.textContent = 'Hide Locations';
|
||||
toggleBtn.classList.add('active');
|
||||
toggleBtn.classList.remove('inactive');
|
||||
}
|
||||
}
|
||||
|
||||
async loadCutLocations() {
|
||||
if (!this.currentCutId) return;
|
||||
|
||||
try {
|
||||
const filters = this.getCurrentFilters();
|
||||
const queryParams = new URLSearchParams(filters);
|
||||
|
||||
const response = await fetch(`/api/cuts/${this.currentCutId}/locations?${queryParams}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load locations: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.currentCutLocations = data.locations || [];
|
||||
|
||||
// Update statistics
|
||||
this.updateStatistics(data.statistics);
|
||||
|
||||
// Display locations on map
|
||||
this.displayLocationsOnMap(this.currentCutLocations);
|
||||
|
||||
this.showingLocations = true;
|
||||
this.showStatus(`Loaded ${this.currentCutLocations.length} locations`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading cut locations:', error);
|
||||
this.showStatus('Failed to load locations', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
displayLocationsOnMap(locations) {
|
||||
// Remove existing markers
|
||||
if (this.locationMarkersLayer) {
|
||||
this.map.removeLayer(this.locationMarkersLayer);
|
||||
}
|
||||
|
||||
// Create new markers layer
|
||||
this.locationMarkersLayer = L.layerGroup();
|
||||
|
||||
locations.forEach(location => {
|
||||
if (location.latitude && location.longitude) {
|
||||
const marker = L.marker([location.latitude, location.longitude], {
|
||||
icon: this.createLocationIcon(location)
|
||||
});
|
||||
|
||||
const popupContent = this.createLocationPopup(location);
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
this.locationMarkersLayer.addLayer(marker);
|
||||
}
|
||||
});
|
||||
|
||||
this.locationMarkersLayer.addTo(this.map);
|
||||
}
|
||||
|
||||
createLocationIcon(location) {
|
||||
// Create different icons based on support level
|
||||
const supportLevel = location.support_level || location['Support Level'] || 'unknown';
|
||||
const colors = {
|
||||
'1': '#28a745', // Green - Strong support
|
||||
'2': '#ffc107', // Yellow - Lean support
|
||||
'3': '#fd7e14', // Orange - Lean opposition
|
||||
'4': '#dc3545', // Red - Strong opposition
|
||||
'unknown': '#6c757d' // Grey - Unknown
|
||||
};
|
||||
|
||||
const color = colors[supportLevel] || colors['unknown'];
|
||||
|
||||
return L.divIcon({
|
||||
className: 'location-marker',
|
||||
html: `<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 saveCutSettings() {
|
||||
if (!this.currentCutId) {
|
||||
this.showStatus('No cut selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = {
|
||||
show_locations: document.getElementById('show-locations-toggle').checked,
|
||||
export_enabled: document.getElementById('export-enabled-toggle').checked,
|
||||
assigned_to: document.getElementById('assigned-to').value,
|
||||
completion_percentage: parseInt(document.getElementById('completion-percentage').value) || 0
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/cuts/${this.currentCutId}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save settings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showStatus('Cut settings saved successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving cut settings:', error);
|
||||
this.showStatus('Failed to save cut settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showLocationManagement(cutId) {
|
||||
console.log('showLocationManagement called with cutId:', cutId);
|
||||
this.currentCutId = cutId;
|
||||
const locationSection = document.getElementById('cut-location-management');
|
||||
console.log('Location section element:', locationSection);
|
||||
|
||||
if (locationSection) {
|
||||
console.log('Setting location section display to block');
|
||||
locationSection.style.display = 'block';
|
||||
|
||||
// Load cut data to populate settings - use multiple possible ID field names for NocoDB
|
||||
const cut = this.cutsManager.allCuts.find(c =>
|
||||
(c.id || c.Id || c.ID || c._id) == cutId
|
||||
);
|
||||
console.log('Found cut for location management:', cut);
|
||||
|
||||
if (cut) {
|
||||
const toggleElement = document.getElementById('show-locations-toggle');
|
||||
const exportElement = document.getElementById('export-enabled-toggle');
|
||||
const assignedElement = document.getElementById('assigned-to');
|
||||
const completionElement = document.getElementById('completion-percentage');
|
||||
|
||||
console.log('Setting up toggles:', {
|
||||
toggleElement: !!toggleElement,
|
||||
exportElement: !!exportElement,
|
||||
assignedElement: !!assignedElement,
|
||||
completionElement: !!completionElement
|
||||
});
|
||||
|
||||
if (toggleElement) toggleElement.checked = (cut.show_locations || cut.Show_locations || cut['Show Locations']) !== false;
|
||||
if (exportElement) exportElement.checked = (cut.export_enabled || cut.Export_enabled || cut['Export Enabled']) !== false;
|
||||
if (assignedElement) assignedElement.value = cut.assigned_to || cut.Assigned_to || cut['Assigned To'] || '';
|
||||
if (completionElement) completionElement.value = cut.completion_percentage || cut.Completion_percentage || cut['Completion Percentage'] || '';
|
||||
}
|
||||
} else {
|
||||
console.error('Location management section not found!');
|
||||
}
|
||||
}
|
||||
|
||||
hideLocationManagement() {
|
||||
const locationSection = document.getElementById('cut-location-management');
|
||||
if (locationSection) {
|
||||
locationSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Clear current data
|
||||
this.currentCutId = null;
|
||||
this.currentCutLocations = [];
|
||||
|
||||
// Hide locations on map
|
||||
if (this.locationMarkersLayer) {
|
||||
this.map.removeLayer(this.locationMarkersLayer);
|
||||
}
|
||||
this.showingLocations = false;
|
||||
|
||||
// Reset button state
|
||||
const toggleBtn = document.getElementById('toggle-location-visibility');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.textContent = 'Show Locations';
|
||||
toggleBtn.classList.remove('active');
|
||||
toggleBtn.classList.remove('inactive');
|
||||
}
|
||||
}
|
||||
|
||||
showStatus(message, type) {
|
||||
// Use existing admin notification system if available
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(message, type);
|
||||
} else if (this.cutsManager && this.cutsManager.showStatus) {
|
||||
this.cutsManager.showStatus(message, type);
|
||||
} else {
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
getSupportColor(supportLevel) {
|
||||
switch(supportLevel) {
|
||||
case '1': return '#28a745'; // Strong Support - Green
|
||||
case '2': return '#ffc107'; // Lean Support - Yellow
|
||||
case '3': return '#fd7e14'; // Oppose - Orange
|
||||
case '4': return '#dc3545'; // Strong Oppose - Red
|
||||
default: return '#6c757d'; // Unknown - Gray
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentFiltersDisplay() {
|
||||
const activeFilters = [];
|
||||
|
||||
// Check location filters
|
||||
const supportFilter = document.getElementById('support-filter')?.value;
|
||||
if (supportFilter && supportFilter !== 'all') {
|
||||
activeFilters.push(`Support Level: ${supportFilter}`);
|
||||
}
|
||||
|
||||
const signFilter = document.getElementById('sign-filter')?.value;
|
||||
if (signFilter && signFilter !== 'all') {
|
||||
activeFilters.push(`Lawn Signs: ${signFilter === 'true' ? 'Yes' : 'No'}`);
|
||||
}
|
||||
|
||||
const contactFilter = document.getElementById('contact-filter')?.value;
|
||||
if (contactFilter && contactFilter !== 'all') {
|
||||
const contactLabels = {
|
||||
'email': 'Has Email',
|
||||
'phone': 'Has Phone',
|
||||
'both': 'Has Both Email & Phone',
|
||||
'none': 'No Contact Info'
|
||||
};
|
||||
activeFilters.push(`Contact: ${contactLabels[contactFilter] || contactFilter}`);
|
||||
}
|
||||
|
||||
if (activeFilters.length === 0) {
|
||||
return '<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;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class if using modules, otherwise it's global
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CutLocationManager;
|
||||
} else {
|
||||
window.CutLocationManager = CutLocationManager;
|
||||
}
|
||||
1052
map/app/public/js/cut-print-utils.js
Normal file
1052
map/app/public/js/cut-print-utils.js
Normal file
File diff suppressed because it is too large
Load Diff
89
map/test_print_debug.html
Normal file
89
map/test_print_debug.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Print Debug Test</title>
|
||||
<script>
|
||||
// Simple test to verify our changes
|
||||
function testPrintUtils() {
|
||||
console.log('Testing CutPrintUtils improvements...');
|
||||
|
||||
// Mock objects for testing
|
||||
const mockMap = {
|
||||
getContainer: () => document.createElement('div'),
|
||||
getBounds: () => ({
|
||||
getNorth: () => 53.6,
|
||||
getSouth: () => 53.4,
|
||||
getEast: () => -113.3,
|
||||
getWest: () => -113.7
|
||||
}),
|
||||
getCenter: () => ({ lat: 53.5, lng: -113.5 }),
|
||||
getZoom: () => 12
|
||||
};
|
||||
|
||||
const mockLocationManager = {
|
||||
currentCutId: '123',
|
||||
currentCutLocations: [
|
||||
{ latitude: 53.5, longitude: -113.5, first_name: 'Test', last_name: 'User', support_level: '1' }
|
||||
],
|
||||
showingLocations: false,
|
||||
loadCutLocations: async () => console.log('Mock loadCutLocations called'),
|
||||
displayLocationsOnMap: (locations) => {
|
||||
console.log('Mock displayLocationsOnMap called with', locations.length, 'locations');
|
||||
mockLocationManager.showingLocations = true;
|
||||
},
|
||||
getSupportColor: (level) => '#28a745'
|
||||
};
|
||||
|
||||
const mockCutsManager = {
|
||||
allCuts: [{ id: '123', name: 'Test Cut' }],
|
||||
currentCutLayer: null
|
||||
};
|
||||
|
||||
// Test our enhanced print utils
|
||||
const printUtils = new CutPrintUtils(mockMap, mockCutsManager, mockLocationManager);
|
||||
|
||||
console.log('CutPrintUtils created successfully with enhanced features');
|
||||
console.log('Available methods:', Object.getOwnPropertyNames(CutPrintUtils.prototype));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run test when page loads
|
||||
window.addEventListener('load', () => {
|
||||
if (typeof CutPrintUtils !== 'undefined') {
|
||||
testPrintUtils();
|
||||
} else {
|
||||
console.log('CutPrintUtils not loaded - this is expected in test environment');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Print Debug Test</h1>
|
||||
<p>Check the browser console for test results.</p>
|
||||
<p>This tests the enhanced CutPrintUtils functionality.</p>
|
||||
|
||||
<h2>Key Improvements Made:</h2>
|
||||
<ul>
|
||||
<li>✅ Auto-load locations when printing if not already loaded</li>
|
||||
<li>✅ Auto-display locations on map for print capture</li>
|
||||
<li>✅ Enhanced map capture with html2canvas (priority #1)</li>
|
||||
<li>✅ Improved dom-to-image capture with better filtering</li>
|
||||
<li>✅ Better UI state management (toggle button updates)</li>
|
||||
<li>✅ Enhanced debugging and logging</li>
|
||||
<li>✅ Auto-show locations when viewing cuts (if enabled)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Root Cause Analysis:</h2>
|
||||
<p>The issue was that locations were not automatically displayed on the map when viewing a cut or printing.
|
||||
The print function expected locations to be visible but they were only shown when the user manually clicked "Show Locations".</p>
|
||||
|
||||
<h2>Solution:</h2>
|
||||
<ol>
|
||||
<li><strong>Print Enhancement:</strong> The print function now ensures locations are loaded and displayed before capturing the map</li>
|
||||
<li><strong>View Enhancement:</strong> When viewing a cut, locations are automatically loaded if the cut has show_locations enabled</li>
|
||||
<li><strong>Capture Enhancement:</strong> Improved map capture methods with html2canvas as primary method</li>
|
||||
<li><strong>State Management:</strong> Better synchronization between location visibility and UI state</li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user