import { Controller } from "@hotwired/stimulus"; import L from "leaflet"; import { showFlashMessage } from "../maps/helpers"; import { applyThemeToButton } from "../maps/theme_utils"; 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; // Listen for theme changes document.addEventListener('theme:changed', this.handleThemeChange.bind(this)); // Wait for the map to be initialized this.waitForMap(); } disconnect() { this.cleanup(); document.removeEventListener('theme:changed', this.handleThemeChange.bind(this)); 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', 'leaflet-control-button add-visit-button'); button.innerHTML = '➕'; button.title = 'Add a visit'; // Style the button with theme-aware styling applyThemeToButton(button, this.userThemeValue || 'dark'); button.style.width = '48px'; button.style.height = '48px'; button.style.borderRadius = '4px'; button.style.padding = '0'; button.style.lineHeight = '48px'; button.style.fontSize = '18px'; button.style.textAlign = 'center'; button.style.transition = 'all 0.2s ease'; // Disable map interactions when clicking the button L.DomEvent.disableClickPropagation(button); // 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 = '#dc3545'; button.style.color = 'white'; button.innerHTML = '✕'; // 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 with theme-aware styling applyThemeToButton(button, this.userThemeValue || 'dark'); button.innerHTML = '➕'; // 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 = `

Add New Visit

`; // 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 })); } }); } handleThemeChange(event) { console.log('Add visit controller: Theme changed to', event.detail.theme); this.userThemeValue = event.detail.theme; // Update button theme if it exists if (this.addVisitButton && !this.isAddingVisit) { applyThemeToButton(this.addVisitButton, this.userThemeValue); } } 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); } } } }