Files
dawarich/app/javascript/controllers/add_visit_controller.js
2025-11-07 10:32:49 +01:00

446 lines
14 KiB
JavaScript

import { Controller } from "@hotwired/stimulus";
import L from "leaflet";
import { showFlashMessage } from "../maps/helpers";
import {
setAddVisitButtonActive,
setAddVisitButtonInactive
} from "../maps/map_controls";
export default class extends Controller {
static targets = [""];
static values = {
apiKey: String,
userTheme: 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;
// The Add Visit button is now created centrally by maps_controller.js
// via addTopRightButtons(). We just need to find it and attach our handler.
setTimeout(() => {
this.addVisitButton = document.querySelector('.add-visit-button');
if (this.addVisitButton) {
// Attach our click handler to the existing button
// Use event capturing and stopPropagation to prevent map click
this.addVisitButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleAddVisitMode(this.addVisitButton);
}, true); // Use capture phase
} else {
console.warn('Add visit button not found, retrying...');
// Retry if button hasn't been created yet
this.addVisitButton = null;
setTimeout(() => this.setupAddVisitButton(), 200);
}
}, 100);
}
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
setAddVisitButtonActive(button);
// Change cursor to crosshair
this.map.getContainer().style.cursor = 'crosshair';
// Add map click listener with a small delay to prevent immediate trigger
// This ensures the button click doesn't propagate to the map
setTimeout(() => {
if (this.isAddingVisit) {
this.map.on('click', this.onMapClick, this);
}
}, 100);
showFlashMessage('notice', 'Click on the map to place a visit');
}
exitAddVisitMode(button) {
this.isAddingVisit = false;
// Reset button style to inactive state
setAddVisitButtonInactive(button, this.userThemeValue || 'dark');
// 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;
} else {
console.warn('No currentPopup reference found');
// Fallback: try to close any open popup
this.map.closePopup();
}
}
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) {
// Close any existing popup first to ensure only one popup is open
if (this.currentPopup) {
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
}
// 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 using DaisyUI classes for automatic theme support
const formHTML = `
<div class="visit-form" style="min-width: 280px;">
<h3 class="text-base font-semibold mb-4">Add New Visit</h3>
<form id="add-visit-form" class="space-y-3">
<div class="form-control">
<label for="visit-name" class="label">
<span class="label-text font-medium">Name:</span>
</label>
<input type="text" id="visit-name" name="name" required
class="input input-bordered w-full"
placeholder="Enter visit name">
</div>
<div class="form-control">
<label for="visit-start" class="label">
<span class="label-text font-medium">Start Time:</span>
</label>
<input type="datetime-local" id="visit-start" name="started_at" required value="${startTime}"
class="input input-bordered w-full">
</div>
<div class="form-control">
<label for="visit-end" class="label">
<span class="label-text font-medium">End Time:</span>
</label>
<input type="datetime-local" id="visit-end" name="ended_at" required value="${endTime}"
class="input input-bordered w-full">
</div>
<input type="hidden" name="latitude" value="${lat}">
<input type="hidden" name="longitude" value="${lng}">
<div class="flex gap-2 mt-4">
<button type="submit" class="btn btn-success flex-1">
Create Visit
</button>
<button type="button" id="cancel-visit" class="btn btn-error flex-1">
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', (e) => {
e.preventDefault();
e.stopPropagation();
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'),
status: 'confirmed' // Manually created visits should be confirmed
}
};
// 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!`);
// Store the created visit data
const createdVisit = data;
this.exitAddVisitMode(this.addVisitButton);
// Add the newly created visit marker immediately to the map
this.addCreatedVisitToMap(createdVisit, visitData.visit.latitude, visitData.visit.longitude);
} 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;
}
}
addCreatedVisitToMap(visitData, latitude, longitude) {
const mapsController = document.querySelector('[data-controller*="maps"]');
if (!mapsController) {
console.log('Could not find maps controller element');
return;
}
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
if (!stimulusController || !stimulusController.visitsManager) {
console.log('Could not find maps controller or visits manager');
return;
}
const visitsManager = stimulusController.visitsManager;
// Create a circle for the newly created visit (always confirmed)
const circle = L.circle([latitude, longitude], {
color: '#4A90E2', // Border color for confirmed visits
fillColor: '#4A90E2', // Fill color for confirmed visits
fillOpacity: 0.5,
radius: 110, // Confirmed visit size
weight: 2,
interactive: true,
bubblingMouseEvents: false,
pane: 'confirmedVisitsPane'
});
// Add the circle to the confirmed visits layer
visitsManager.confirmedVisitCircles.addLayer(circle);
// Make sure the layer is visible on the map
if (!this.map.hasLayer(visitsManager.confirmedVisitCircles)) {
this.map.addLayer(visitsManager.confirmedVisitCircles);
}
// Check if the layer control has the confirmed visits layer enabled
this.ensureConfirmedVisitsLayerEnabled();
}
ensureConfirmedVisitsLayerEnabled() {
// Find the layer control and check/enable the "Confirmed Visits" checkbox
const layerControlContainer = document.querySelector('.leaflet-control-layers');
if (!layerControlContainer) {
console.log('Layer control container not found');
return;
}
// Expand the layer control if it's collapsed
const layerControlExpand = layerControlContainer.querySelector('.leaflet-control-layers-toggle');
if (layerControlExpand) {
layerControlExpand.click();
}
setTimeout(() => {
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
inputs.forEach(input => {
const label = input.nextElementSibling;
if (label && label.textContent.trim().includes('Confirmed Visits')) {
if (!input.checked) {
input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
}, 100);
}
refreshVisitsLayer() {
// Don't auto-refresh after creating a visit
// The visit is already visible on the map from addCreatedVisitToMap()
// Auto-refresh would clear it because fetchAndDisplayVisits uses URL date params
// which might not include the newly created visit
console.log('Skipping auto-refresh - visit already added to map');
}
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);
}
}
}
}