mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Add visits manual creation
This commit is contained in:
parent
4b55e1b29a
commit
1da3ef5c44
8 changed files with 584 additions and 5 deletions
File diff suppressed because one or more lines are too long
|
|
@ -33,6 +33,40 @@
|
|||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Add Visit Marker Styles */
|
||||
.add-visit-marker {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
background: white;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
|
||||
animation: pulse-visit 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-visit {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Visit Form Popup Styles */
|
||||
.visit-form-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.leaflet-right-panel.controls-shifted {
|
||||
right: 310px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,37 @@ class Api::V1::VisitsController < ApiController
|
|||
render json: serialized_visits
|
||||
end
|
||||
|
||||
def create
|
||||
visit = current_api_user.visits.build(visit_params.except(:latitude, :longitude))
|
||||
|
||||
# If coordinates are provided but no place_id, create a place
|
||||
if visit_params[:latitude].present? && visit_params[:longitude].present? && visit.place_id.blank?
|
||||
place = create_place_from_coordinates(visit_params[:latitude], visit_params[:longitude], visit_params[:name])
|
||||
if place
|
||||
visit.place = place
|
||||
else
|
||||
return render json: { error: 'Failed to create place for visit' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Validate that visit has a place
|
||||
if visit.place.blank?
|
||||
return render json: { error: 'Visit must have a valid place' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
# Set visit times and calculate duration
|
||||
visit.started_at = DateTime.parse(visit_params[:started_at])
|
||||
visit.ended_at = DateTime.parse(visit_params[:ended_at])
|
||||
visit.duration = (visit.ended_at - visit.started_at) * 24 * 60 # duration in minutes
|
||||
|
||||
# Set status to confirmed for manually created visits
|
||||
visit.status = :confirmed
|
||||
|
||||
visit.save!
|
||||
|
||||
render json: Api::VisitSerializer.new(visit).call
|
||||
end
|
||||
|
||||
def update
|
||||
visit = current_api_user.visits.find(params[:id])
|
||||
visit = update_visit(visit)
|
||||
|
|
@ -65,7 +96,7 @@ class Api::V1::VisitsController < ApiController
|
|||
private
|
||||
|
||||
def visit_params
|
||||
params.require(:visit).permit(:name, :place_id, :status)
|
||||
params.require(:visit).permit(:name, :place_id, :status, :latitude, :longitude, :started_at, :ended_at)
|
||||
end
|
||||
|
||||
def merge_params
|
||||
|
|
@ -76,8 +107,55 @@ class Api::V1::VisitsController < ApiController
|
|||
params.permit(:status, visit_ids: [])
|
||||
end
|
||||
|
||||
def create_place_from_coordinates(latitude, longitude, name)
|
||||
Rails.logger.info "Creating place from coordinates: lat=#{latitude}, lon=#{longitude}, name=#{name}"
|
||||
|
||||
# Create a place at the specified coordinates
|
||||
place_name = name.presence || Place::DEFAULT_NAME
|
||||
|
||||
# Validate coordinates
|
||||
lat_f = latitude.to_f
|
||||
lon_f = longitude.to_f
|
||||
|
||||
if lat_f.abs > 90 || lon_f.abs > 180
|
||||
Rails.logger.error "Invalid coordinates: lat=#{lat_f}, lon=#{lon_f}"
|
||||
return nil
|
||||
end
|
||||
|
||||
# Check if a place already exists very close to these coordinates (within 10 meters)
|
||||
existing_place = Place.joins("JOIN visits ON places.id = visits.place_id")
|
||||
.where(visits: { user: current_api_user })
|
||||
.where(
|
||||
"ST_DWithin(lonlat, ST_SetSRID(ST_MakePoint(?, ?), 4326), ?)",
|
||||
lon_f, lat_f, 0.0001 # approximately 10 meters
|
||||
).first
|
||||
|
||||
if existing_place
|
||||
Rails.logger.info "Found existing place: #{existing_place.id}"
|
||||
return existing_place
|
||||
end
|
||||
|
||||
# Create new place with both coordinate formats
|
||||
place = Place.create!(
|
||||
name: place_name,
|
||||
latitude: lat_f,
|
||||
longitude: lon_f,
|
||||
lonlat: "POINT(#{lon_f} #{lat_f})",
|
||||
source: :manual
|
||||
)
|
||||
|
||||
Rails.logger.info "Created new place: #{place.id} at #{place.lonlat}"
|
||||
place
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to create place: #{e.class} - #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n") if Rails.env.development?
|
||||
nil
|
||||
end
|
||||
|
||||
def update_visit(visit)
|
||||
visit_params.each do |key, value|
|
||||
next if %w[latitude longitude].include?(key.to_s)
|
||||
|
||||
visit[key] = value
|
||||
visit.name = visit.place.name if visit_params[:place_id].present?
|
||||
end
|
||||
|
|
|
|||
465
app/javascript/controllers/add_visit_controller.js
Normal file
465
app/javascript/controllers/add_visit_controller.js
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import { showFlashMessage } from "../maps/helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [""];
|
||||
static values = {
|
||||
apiKey: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Add visit controller connected");
|
||||
this.map = null;
|
||||
this.isAddingVisit = false;
|
||||
this.addVisitMarker = null;
|
||||
this.addVisitButton = null;
|
||||
this.currentPopup = null;
|
||||
this.mapsController = null;
|
||||
|
||||
// Wait for the map to be initialized
|
||||
this.waitForMap();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanup();
|
||||
console.log("Add visit controller disconnected");
|
||||
}
|
||||
|
||||
waitForMap() {
|
||||
// Get the map from the maps controller instance
|
||||
const mapElement = document.querySelector('[data-controller*="maps"]');
|
||||
|
||||
if (mapElement) {
|
||||
// Try to get Stimulus controller instance
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapElement, 'maps');
|
||||
if (stimulusController && stimulusController.map) {
|
||||
this.map = stimulusController.map;
|
||||
this.mapsController = stimulusController;
|
||||
this.apiKeyValue = stimulusController.apiKey;
|
||||
this.setupAddVisitButton();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check for map container and try to find map instance
|
||||
const mapContainer = document.getElementById('map');
|
||||
if (mapContainer && mapContainer._leaflet_id) {
|
||||
// Get map instance from Leaflet registry
|
||||
this.map = window.L._getMap ? window.L._getMap(mapContainer._leaflet_id) : null;
|
||||
|
||||
if (!this.map) {
|
||||
// Try through Leaflet internal registry
|
||||
const maps = window.L.Map._instances || {};
|
||||
this.map = maps[mapContainer._leaflet_id];
|
||||
}
|
||||
|
||||
if (this.map) {
|
||||
// Get API key from map element data
|
||||
this.apiKeyValue = mapContainer.dataset.api_key || this.element.dataset.apiKey;
|
||||
this.setupAddVisitButton();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a bit more for the map to initialize
|
||||
setTimeout(() => this.waitForMap(), 200);
|
||||
}
|
||||
|
||||
setupAddVisitButton() {
|
||||
if (!this.map || this.addVisitButton) return;
|
||||
|
||||
// Create the Add Visit control
|
||||
const AddVisitControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'add-visit-button');
|
||||
button.innerHTML = '📍 Add a Visit';
|
||||
button.title = 'Click to add a visit to the map';
|
||||
|
||||
// Style the button
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.padding = '8px 12px';
|
||||
button.style.border = '2px solid #ccc';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
button.style.fontSize = '14px';
|
||||
button.style.fontWeight = 'bold';
|
||||
button.style.marginBottom = '5px';
|
||||
button.style.display = 'block';
|
||||
button.style.width = '100%';
|
||||
button.style.textAlign = 'center';
|
||||
button.style.transition = 'all 0.2s ease';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Add hover effects
|
||||
button.addEventListener('mouseenter', () => {
|
||||
if (!this.isAddingVisit) {
|
||||
button.style.backgroundColor = '#f0f0f0';
|
||||
}
|
||||
});
|
||||
|
||||
button.addEventListener('mouseleave', () => {
|
||||
if (!this.isAddingVisit) {
|
||||
button.style.backgroundColor = 'white';
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle add visit mode on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
this.toggleAddVisitMode(button);
|
||||
});
|
||||
|
||||
this.addVisitButton = button;
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map (top right, below existing buttons)
|
||||
this.map.addControl(new AddVisitControl({ position: 'topright' }));
|
||||
}
|
||||
|
||||
toggleAddVisitMode(button) {
|
||||
if (this.isAddingVisit) {
|
||||
// Exit add visit mode
|
||||
this.exitAddVisitMode(button);
|
||||
} else {
|
||||
// Enter add visit mode
|
||||
this.enterAddVisitMode(button);
|
||||
}
|
||||
}
|
||||
|
||||
enterAddVisitMode(button) {
|
||||
this.isAddingVisit = true;
|
||||
|
||||
// Update button style to show active state
|
||||
button.style.backgroundColor = '#007bff';
|
||||
button.style.color = 'white';
|
||||
button.style.borderColor = '#0056b3';
|
||||
button.innerHTML = '✕ Cancel';
|
||||
|
||||
// Change cursor to crosshair
|
||||
this.map.getContainer().style.cursor = 'crosshair';
|
||||
|
||||
// Add map click listener
|
||||
this.map.on('click', this.onMapClick, this);
|
||||
|
||||
showFlashMessage('notice', 'Click on the map to place a visit');
|
||||
}
|
||||
|
||||
exitAddVisitMode(button) {
|
||||
this.isAddingVisit = false;
|
||||
|
||||
// Reset button style
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.color = 'black';
|
||||
button.style.borderColor = '#ccc';
|
||||
button.innerHTML = '📍 Add Visit';
|
||||
|
||||
// Reset cursor
|
||||
this.map.getContainer().style.cursor = '';
|
||||
|
||||
// Remove map click listener
|
||||
this.map.off('click', this.onMapClick, this);
|
||||
|
||||
// Remove any existing marker
|
||||
if (this.addVisitMarker) {
|
||||
this.map.removeLayer(this.addVisitMarker);
|
||||
this.addVisitMarker = null;
|
||||
}
|
||||
|
||||
// Close any open popup
|
||||
if (this.currentPopup) {
|
||||
this.map.closePopup(this.currentPopup);
|
||||
this.currentPopup = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMapClick(e) {
|
||||
if (!this.isAddingVisit) return;
|
||||
|
||||
const { lat, lng } = e.latlng;
|
||||
|
||||
// Remove existing marker if any
|
||||
if (this.addVisitMarker) {
|
||||
this.map.removeLayer(this.addVisitMarker);
|
||||
}
|
||||
|
||||
// Create a new marker at the clicked location
|
||||
this.addVisitMarker = L.marker([lat, lng], {
|
||||
draggable: true,
|
||||
icon: L.divIcon({
|
||||
className: 'add-visit-marker',
|
||||
html: '📍',
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15]
|
||||
})
|
||||
}).addTo(this.map);
|
||||
|
||||
// Show the visit form popup
|
||||
this.showVisitForm(lat, lng);
|
||||
}
|
||||
|
||||
showVisitForm(lat, lng) {
|
||||
// Get current date/time for default values
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000));
|
||||
|
||||
// Format dates for datetime-local input
|
||||
const formatDateTime = (date) => {
|
||||
return date.toISOString().slice(0, 16);
|
||||
};
|
||||
|
||||
const startTime = formatDateTime(now);
|
||||
const endTime = formatDateTime(oneHourLater);
|
||||
|
||||
// Create form HTML
|
||||
const formHTML = `
|
||||
<div class="visit-form" style="min-width: 280px;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #333;">Add New Visit</h3>
|
||||
|
||||
<form id="add-visit-form" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div>
|
||||
<label for="visit-name" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">Name:</label>
|
||||
<input type="text" id="visit-name" name="name" required
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;"
|
||||
placeholder="Enter visit name">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="visit-start" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">Start Time:</label>
|
||||
<input type="datetime-local" id="visit-start" name="started_at" required value="${startTime}"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="visit-end" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">End Time:</label>
|
||||
<input type="datetime-local" id="visit-end" name="ended_at" required value="${endTime}"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="latitude" value="${lat}">
|
||||
<input type="hidden" name="longitude" value="${lng}">
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||||
<button type="submit" style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
Create Visit
|
||||
</button>
|
||||
<button type="button" id="cancel-visit" style="flex: 1; background: #dc3545; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create popup at the marker location
|
||||
this.currentPopup = L.popup({
|
||||
closeOnClick: false,
|
||||
autoClose: false,
|
||||
maxWidth: 300,
|
||||
className: 'visit-form-popup'
|
||||
})
|
||||
.setLatLng([lat, lng])
|
||||
.setContent(formHTML)
|
||||
.openOn(this.map);
|
||||
|
||||
// Add event listeners after the popup is added to DOM
|
||||
setTimeout(() => {
|
||||
const form = document.getElementById('add-visit-form');
|
||||
const cancelButton = document.getElementById('cancel-visit');
|
||||
const nameInput = document.getElementById('visit-name');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
||||
}
|
||||
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.exitAddVisitMode(this.addVisitButton);
|
||||
});
|
||||
}
|
||||
|
||||
// Focus the name input
|
||||
if (nameInput) {
|
||||
nameInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Get form values
|
||||
const visitData = {
|
||||
visit: {
|
||||
name: formData.get('name'),
|
||||
started_at: formData.get('started_at'),
|
||||
ended_at: formData.get('ended_at'),
|
||||
latitude: formData.get('latitude'),
|
||||
longitude: formData.get('longitude')
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that end time is after start time
|
||||
const startTime = new Date(visitData.visit.started_at);
|
||||
const endTime = new Date(visitData.visit.ended_at);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
showFlashMessage('error', 'End time must be after start time');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable form while submitting
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Creating...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/visits`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKeyValue}`
|
||||
},
|
||||
body: JSON.stringify(visitData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showFlashMessage('notice', `Visit "${visitData.visit.name}" created successfully!`);
|
||||
this.exitAddVisitMode(this.addVisitButton);
|
||||
|
||||
// Refresh visits layer - this will clear and refetch data
|
||||
this.refreshVisitsLayer();
|
||||
|
||||
// Ensure confirmed visits layer is enabled (with a small delay for the API call to complete)
|
||||
setTimeout(() => {
|
||||
this.ensureVisitsLayersEnabled();
|
||||
}, 300);
|
||||
} else {
|
||||
const errorMessage = data.error || data.message || 'Failed to create visit';
|
||||
showFlashMessage('error', errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating visit:', error);
|
||||
showFlashMessage('error', 'Network error: Failed to create visit');
|
||||
} finally {
|
||||
// Re-enable form
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
refreshVisitsLayer() {
|
||||
console.log('Attempting to refresh visits layer...');
|
||||
|
||||
// Try multiple approaches to refresh the visits layer
|
||||
const mapsController = document.querySelector('[data-controller*="maps"]');
|
||||
if (mapsController) {
|
||||
// Try to get the Stimulus controller instance
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
|
||||
|
||||
if (stimulusController && stimulusController.visitsManager) {
|
||||
console.log('Found maps controller with visits manager');
|
||||
|
||||
// Clear existing visits and fetch fresh data
|
||||
if (stimulusController.visitsManager.visitCircles) {
|
||||
stimulusController.visitsManager.visitCircles.clearLayers();
|
||||
}
|
||||
if (stimulusController.visitsManager.confirmedVisitCircles) {
|
||||
stimulusController.visitsManager.confirmedVisitCircles.clearLayers();
|
||||
}
|
||||
|
||||
// Refresh the visits data
|
||||
if (typeof stimulusController.visitsManager.fetchAndDisplayVisits === 'function') {
|
||||
console.log('Refreshing visits data...');
|
||||
stimulusController.visitsManager.fetchAndDisplayVisits();
|
||||
}
|
||||
} else {
|
||||
console.log('Could not find maps controller or visits manager');
|
||||
|
||||
// Fallback: Try to dispatch a custom event
|
||||
const refreshEvent = new CustomEvent('visits:refresh', { bubbles: true });
|
||||
mapsController.dispatchEvent(refreshEvent);
|
||||
}
|
||||
} else {
|
||||
console.log('Could not find maps controller element');
|
||||
}
|
||||
}
|
||||
|
||||
ensureVisitsLayersEnabled() {
|
||||
console.log('Ensuring visits layers are enabled...');
|
||||
|
||||
const mapsController = document.querySelector('[data-controller*="maps"]');
|
||||
if (mapsController) {
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
|
||||
|
||||
if (stimulusController && stimulusController.map && stimulusController.visitsManager) {
|
||||
const map = stimulusController.map;
|
||||
const visitsManager = stimulusController.visitsManager;
|
||||
|
||||
// Get the confirmed visits layer (newly created visits are always confirmed)
|
||||
const confirmedVisitsLayer = visitsManager.getConfirmedVisitCirclesLayer();
|
||||
|
||||
// Ensure confirmed visits layer is added to map since we create confirmed visits
|
||||
if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) {
|
||||
console.log('Adding confirmed visits layer to map');
|
||||
map.addLayer(confirmedVisitsLayer);
|
||||
|
||||
// Update the layer control checkbox to reflect the layer is now active
|
||||
this.updateLayerControlCheckbox('Confirmed Visits', true);
|
||||
}
|
||||
|
||||
// Refresh visits data to include the new visit
|
||||
if (typeof visitsManager.fetchAndDisplayVisits === 'function') {
|
||||
console.log('Final refresh of visits to show new visit...');
|
||||
visitsManager.fetchAndDisplayVisits();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLayerControlCheckbox(layerName, isEnabled) {
|
||||
// Find the layer control input for the specified layer
|
||||
const layerControlContainer = document.querySelector('.leaflet-control-layers');
|
||||
if (!layerControlContainer) {
|
||||
console.log('Layer control container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.nextElementSibling;
|
||||
if (label && label.textContent.trim() === layerName) {
|
||||
console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
|
||||
input.checked = isEnabled;
|
||||
|
||||
// Trigger change event to ensure proper state management
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.map) {
|
||||
this.map.off('click', this.onMapClick, this);
|
||||
|
||||
if (this.addVisitMarker) {
|
||||
this.map.removeLayer(this.addVisitMarker);
|
||||
}
|
||||
|
||||
if (this.currentPopup) {
|
||||
this.map.closePopup(this.currentPopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -645,7 +645,7 @@ export default class extends BaseController {
|
|||
const markerId = parseInt(marker[6]);
|
||||
return markerId !== numericId;
|
||||
});
|
||||
|
||||
|
||||
// Update scratch layer manager with updated markers
|
||||
if (this.scratchLayerManager) {
|
||||
this.scratchLayerManager.updateMarkers(this.markers);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ class Imports::Create
|
|||
import.update!(status: :failed)
|
||||
broadcast_status_update
|
||||
|
||||
ExceptionReporter.call(e, 'Import failed')
|
||||
|
||||
create_import_failed_notification(import, user, e)
|
||||
ensure
|
||||
if import.processing?
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
<div
|
||||
id='map'
|
||||
class="w-full z-0"
|
||||
data-controller="maps points"
|
||||
data-controller="maps points add-visit"
|
||||
data-points-target="map"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-self_hosted="<%= @self_hosted %>"
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :areas, only: %i[index create update destroy]
|
||||
resources :points, only: %i[index create update destroy]
|
||||
resources :visits, only: %i[index update] do
|
||||
resources :visits, only: %i[index create update] do
|
||||
get 'possible_places', to: 'visits/possible_places#index', on: :member
|
||||
collection do
|
||||
post 'merge', to: 'visits#merge'
|
||||
|
|
|
|||
Loading…
Reference in a new issue