dawarich/app/javascript/controllers/add_visit_controller.js

508 lines
16 KiB
JavaScript
Raw Normal View History

2025-08-21 12:42:45 -04:00
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,
};
2025-08-21 12:42:45 -04:00
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"
);
2025-08-21 12:42:45 -04:00
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");
2025-08-21 12:42:45 -04:00
if (mapContainer && mapContainer._leaflet_id) {
// Get map instance from Leaflet registry
this.map = window.L._getMap
? window.L._getMap(mapContainer._leaflet_id)
: null;
2025-08-21 12:42:45 -04:00
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;
2025-08-21 12:42:45 -04:00
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";
2025-08-21 14:41:53 -04:00
// Style the button to match other map controls
button.style.width = "48px";
button.style.height = "48px";
button.style.border = "none";
button.style.cursor = "pointer";
button.style.boxShadow = "0 1px 4px rgba(0,0,0,0.3)";
button.style.backgroundColor = "white";
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";
2025-08-21 12:42:45 -04:00
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);
// Add hover effects
button.addEventListener("mouseenter", () => {
2025-08-21 12:42:45 -04:00
if (!this.isAddingVisit) {
button.style.backgroundColor = "#f0f0f0";
2025-08-21 12:42:45 -04:00
}
});
button.addEventListener("mouseleave", () => {
2025-08-21 12:42:45 -04:00
if (!this.isAddingVisit) {
button.style.backgroundColor = "white";
2025-08-21 12:42:45 -04:00
}
});
// Toggle add visit mode on button click
L.DomEvent.on(button, "click", () => {
2025-08-21 12:42:45 -04:00
this.toggleAddVisitMode(button);
});
this.addVisitButton = button;
return button;
},
2025-08-21 12:42:45 -04:00
});
// Add the control to the map (top right, below existing buttons)
this.map.addControl(new AddVisitControl({ position: "topright" }));
2025-08-21 12:42:45 -04:00
}
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 = "✕";
2025-08-21 12:42:45 -04:00
// Change cursor to crosshair
this.map.getContainer().style.cursor = "crosshair";
2025-08-21 12:42:45 -04:00
// Add map click listener
this.map.on("click", this.onMapClick, this);
2025-08-21 12:42:45 -04:00
showFlashMessage("notice", "Click on the map to place a visit");
2025-08-21 12:42:45 -04:00
}
exitAddVisitMode(button) {
this.isAddingVisit = false;
// Reset button style
button.style.backgroundColor = "white";
button.style.color = "black";
button.innerHTML = "";
2025-08-21 12:42:45 -04:00
// Reset cursor
this.map.getContainer().style.cursor = "";
2025-08-21 12:42:45 -04:00
// Remove map click listener
this.map.off("click", this.onMapClick, this);
2025-08-21 12:42:45 -04:00
// 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: "📍",
2025-08-21 12:42:45 -04:00
iconSize: [30, 30],
iconAnchor: [15, 15],
}),
2025-08-21 12:42:45 -04:00
}).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);
2025-08-21 12:42:45 -04:00
// Format dates for datetime-local input (2011-10-05T14:48) (YYYY-MM-DDTHH:mm)
2025-08-21 12:42:45 -04:00
const formatDateTime = (date) => {
const pad = (n) => n.toString().padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
date.getDate()
)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
2025-08-21 12:42:45 -04:00
};
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",
2025-08-21 12:42:45 -04:00
})
.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");
2025-08-21 12:42:45 -04:00
if (form) {
form.addEventListener("submit", (e) => this.handleFormSubmit(e));
2025-08-21 12:42:45 -04:00
}
if (cancelButton) {
cancelButton.addEventListener("click", () => {
2025-08-21 12:42:45 -04:00
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);
// Convert local datetime to UTC ISO string
const localToUTCISOString = (localDateTimeString) => {
const local = new Date(localDateTimeString);
return local.toISOString(); // always UTC
};
2025-08-21 12:42:45 -04:00
// Get form values
const visitData = {
visit: {
name: formData.get("name"),
started_at: localToUTCISOString(formData.get("started_at")),
ended_at: localToUTCISOString(formData.get("ended_at")),
latitude: formData.get("latitude"),
longitude: formData.get("longitude"),
},
2025-08-21 12:42:45 -04:00
};
// 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");
2025-08-21 12:42:45 -04:00
return;
}
// Disable form while submitting
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.disabled = true;
submitButton.textContent = "Creating...";
2025-08-21 12:42:45 -04:00
try {
const response = await fetch(`/api/v1/visits`, {
method: "POST",
2025-08-21 12:42:45 -04:00
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${this.apiKeyValue}`,
2025-08-21 12:42:45 -04:00
},
body: JSON.stringify(visitData),
2025-08-21 12:42:45 -04:00
});
const data = await response.json();
if (response.ok) {
showFlashMessage(
"notice",
`Visit "${visitData.visit.name}" created successfully!`
);
2025-08-21 12:42:45 -04:00
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);
2025-08-21 12:42:45 -04:00
}
} catch (error) {
console.error("Error creating visit:", error);
showFlashMessage("error", "Network error: Failed to create visit");
2025-08-21 12:42:45 -04:00
} finally {
// Re-enable form
submitButton.disabled = false;
submitButton.textContent = originalText;
}
}
refreshVisitsLayer() {
console.log("Attempting to refresh visits layer...");
2025-08-21 12:42:45 -04:00
// 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"
);
2025-08-21 12:42:45 -04:00
if (stimulusController && stimulusController.visitsManager) {
console.log("Found maps controller with visits manager");
2025-08-21 14:41:53 -04:00
2025-08-21 12:42:45 -04:00
// 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...");
2025-08-21 12:42:45 -04:00
stimulusController.visitsManager.fetchAndDisplayVisits();
}
} else {
console.log("Could not find maps controller or visits manager");
2025-08-21 14:41:53 -04:00
2025-08-21 12:42:45 -04:00
// Fallback: Try to dispatch a custom event
const refreshEvent = new CustomEvent("visits:refresh", {
bubbles: true,
});
2025-08-21 12:42:45 -04:00
mapsController.dispatchEvent(refreshEvent);
}
} else {
console.log("Could not find maps controller element");
2025-08-21 12:42:45 -04:00
}
}
ensureVisitsLayersEnabled() {
console.log("Ensuring visits layers are enabled...");
2025-08-21 12:42:45 -04:00
const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) {
const stimulusController =
this.application.getControllerForElementAndIdentifier(
mapsController,
"maps"
);
if (
stimulusController &&
stimulusController.map &&
stimulusController.visitsManager
) {
2025-08-21 12:42:45 -04:00
const map = stimulusController.map;
const visitsManager = stimulusController.visitsManager;
// Get the confirmed visits layer (newly created visits are always confirmed)
const confirmedVisitsLayer =
visitsManager.getConfirmedVisitCirclesLayer();
2025-08-21 12:42:45 -04:00
// 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");
2025-08-21 12:42:45 -04:00
map.addLayer(confirmedVisitsLayer);
2025-08-21 14:41:53 -04:00
2025-08-21 12:42:45 -04:00
// Update the layer control checkbox to reflect the layer is now active
this.updateLayerControlCheckbox("Confirmed Visits", true);
2025-08-21 12:42:45 -04:00
}
// Refresh visits data to include the new visit
if (typeof visitsManager.fetchAndDisplayVisits === "function") {
console.log("Final refresh of visits to show new visit...");
2025-08-21 12:42:45 -04:00
visitsManager.fetchAndDisplayVisits();
}
}
}
}
updateLayerControlCheckbox(layerName, isEnabled) {
// Find the layer control input for the specified layer
const layerControlContainer = document.querySelector(
".leaflet-control-layers"
);
2025-08-21 12:42:45 -04:00
if (!layerControlContainer) {
console.log("Layer control container not found");
2025-08-21 12:42:45 -04:00
return;
}
const inputs = layerControlContainer.querySelectorAll(
'input[type="checkbox"]'
);
inputs.forEach((input) => {
2025-08-21 12:42:45 -04:00
const label = input.nextElementSibling;
if (label && label.textContent.trim() === layerName) {
console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
input.checked = isEnabled;
2025-08-21 14:41:53 -04:00
2025-08-21 12:42:45 -04:00
// Trigger change event to ensure proper state management
input.dispatchEvent(new Event("change", { bubbles: true }));
2025-08-21 12:42:45 -04:00
}
});
}
cleanup() {
if (this.map) {
this.map.off("click", this.onMapClick, this);
2025-08-21 12:42:45 -04:00
if (this.addVisitMarker) {
this.map.removeLayer(this.addVisitMarker);
}
if (this.currentPopup) {
this.map.closePopup(this.currentPopup);
}
}
}
}