mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 17:51:39 -05:00
507 lines
16 KiB
JavaScript
507 lines
16 KiB
JavaScript
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",
|
||
"leaflet-control-button add-visit-button"
|
||
);
|
||
button.innerHTML = "➕";
|
||
button.title = "Add a visit";
|
||
|
||
// 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";
|
||
|
||
// 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 = "#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
|
||
button.style.backgroundColor = "white";
|
||
button.style.color = "black";
|
||
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 (2011-10-05T14:48) (YYYY-MM-DDTHH:mm)
|
||
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())}`;
|
||
};
|
||
|
||
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);
|
||
|
||
// Convert local datetime to UTC ISO string
|
||
const localToUTCISOString = (localDateTimeString) => {
|
||
const local = new Date(localDateTimeString);
|
||
return local.toISOString(); // always UTC
|
||
};
|
||
|
||
// 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"),
|
||
},
|
||
};
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
}
|
||
}
|