Fix UTC/Local time issue when creating a visit

This commit is contained in:
Xantin 2025-08-24 00:01:53 +02:00
parent 758b31d6bd
commit f0e8c211ff

View file

@ -5,8 +5,8 @@ import { showFlashMessage } from "../maps/helpers";
export default class extends Controller { export default class extends Controller {
static targets = [""]; static targets = [""];
static values = { static values = {
apiKey: String apiKey: String,
} };
connect() { connect() {
console.log("Add visit controller connected"); console.log("Add visit controller connected");
@ -32,7 +32,11 @@ export default class extends Controller {
if (mapElement) { if (mapElement) {
// Try to get Stimulus controller instance // Try to get Stimulus controller instance
const stimulusController = this.application.getControllerForElementAndIdentifier(mapElement, 'maps'); const stimulusController =
this.application.getControllerForElementAndIdentifier(
mapElement,
"maps"
);
if (stimulusController && stimulusController.map) { if (stimulusController && stimulusController.map) {
this.map = stimulusController.map; this.map = stimulusController.map;
this.mapsController = stimulusController; this.mapsController = stimulusController;
@ -43,10 +47,12 @@ export default class extends Controller {
} }
// Fallback: check for map container and try to find map instance // Fallback: check for map container and try to find map instance
const mapContainer = document.getElementById('map'); const mapContainer = document.getElementById("map");
if (mapContainer && mapContainer._leaflet_id) { if (mapContainer && mapContainer._leaflet_id) {
// Get map instance from Leaflet registry // Get map instance from Leaflet registry
this.map = window.L._getMap ? window.L._getMap(mapContainer._leaflet_id) : null; this.map = window.L._getMap
? window.L._getMap(mapContainer._leaflet_id)
: null;
if (!this.map) { if (!this.map) {
// Try through Leaflet internal registry // Try through Leaflet internal registry
@ -56,7 +62,8 @@ export default class extends Controller {
if (this.map) { if (this.map) {
// Get API key from map element data // Get API key from map element data
this.apiKeyValue = mapContainer.dataset.api_key || this.element.dataset.apiKey; this.apiKeyValue =
mapContainer.dataset.api_key || this.element.dataset.apiKey;
this.setupAddVisitButton(); this.setupAddVisitButton();
return; return;
} }
@ -72,52 +79,55 @@ export default class extends Controller {
// Create the Add Visit control // Create the Add Visit control
const AddVisitControl = L.Control.extend({ const AddVisitControl = L.Control.extend({
onAdd: (map) => { onAdd: (map) => {
const button = L.DomUtil.create('button', 'leaflet-control-button add-visit-button'); const button = L.DomUtil.create(
button.innerHTML = ''; "button",
button.title = 'Add a visit'; "leaflet-control-button add-visit-button"
);
button.innerHTML = "";
button.title = "Add a visit";
// Style the button to match other map controls // Style the button to match other map controls
button.style.width = '48px'; button.style.width = "48px";
button.style.height = '48px'; button.style.height = "48px";
button.style.border = 'none'; button.style.border = "none";
button.style.cursor = 'pointer'; button.style.cursor = "pointer";
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; button.style.boxShadow = "0 1px 4px rgba(0,0,0,0.3)";
button.style.backgroundColor = 'white'; button.style.backgroundColor = "white";
button.style.borderRadius = '4px'; button.style.borderRadius = "4px";
button.style.padding = '0'; button.style.padding = "0";
button.style.lineHeight = '48px'; button.style.lineHeight = "48px";
button.style.fontSize = '18px'; button.style.fontSize = "18px";
button.style.textAlign = 'center'; button.style.textAlign = "center";
button.style.transition = 'all 0.2s ease'; button.style.transition = "all 0.2s ease";
// Disable map interactions when clicking the button // Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button); L.DomEvent.disableClickPropagation(button);
// Add hover effects // Add hover effects
button.addEventListener('mouseenter', () => { button.addEventListener("mouseenter", () => {
if (!this.isAddingVisit) { if (!this.isAddingVisit) {
button.style.backgroundColor = '#f0f0f0'; button.style.backgroundColor = "#f0f0f0";
} }
}); });
button.addEventListener('mouseleave', () => { button.addEventListener("mouseleave", () => {
if (!this.isAddingVisit) { if (!this.isAddingVisit) {
button.style.backgroundColor = 'white'; button.style.backgroundColor = "white";
} }
}); });
// Toggle add visit mode on button click // Toggle add visit mode on button click
L.DomEvent.on(button, 'click', () => { L.DomEvent.on(button, "click", () => {
this.toggleAddVisitMode(button); this.toggleAddVisitMode(button);
}); });
this.addVisitButton = button; this.addVisitButton = button;
return button; return button;
} },
}); });
// Add the control to the map (top right, below existing buttons) // Add the control to the map (top right, below existing buttons)
this.map.addControl(new AddVisitControl({ position: 'topright' })); this.map.addControl(new AddVisitControl({ position: "topright" }));
} }
toggleAddVisitMode(button) { toggleAddVisitMode(button) {
@ -134,32 +144,32 @@ export default class extends Controller {
this.isAddingVisit = true; this.isAddingVisit = true;
// Update button style to show active state // Update button style to show active state
button.style.backgroundColor = '#dc3545'; button.style.backgroundColor = "#dc3545";
button.style.color = 'white'; button.style.color = "white";
button.innerHTML = '✕'; button.innerHTML = "✕";
// Change cursor to crosshair // Change cursor to crosshair
this.map.getContainer().style.cursor = 'crosshair'; this.map.getContainer().style.cursor = "crosshair";
// Add map click listener // Add map click listener
this.map.on('click', this.onMapClick, this); this.map.on("click", this.onMapClick, this);
showFlashMessage('notice', 'Click on the map to place a visit'); showFlashMessage("notice", "Click on the map to place a visit");
} }
exitAddVisitMode(button) { exitAddVisitMode(button) {
this.isAddingVisit = false; this.isAddingVisit = false;
// Reset button style // Reset button style
button.style.backgroundColor = 'white'; button.style.backgroundColor = "white";
button.style.color = 'black'; button.style.color = "black";
button.innerHTML = ''; button.innerHTML = "";
// Reset cursor // Reset cursor
this.map.getContainer().style.cursor = ''; this.map.getContainer().style.cursor = "";
// Remove map click listener // Remove map click listener
this.map.off('click', this.onMapClick, this); this.map.off("click", this.onMapClick, this);
// Remove any existing marker // Remove any existing marker
if (this.addVisitMarker) { if (this.addVisitMarker) {
@ -188,11 +198,11 @@ export default class extends Controller {
this.addVisitMarker = L.marker([lat, lng], { this.addVisitMarker = L.marker([lat, lng], {
draggable: true, draggable: true,
icon: L.divIcon({ icon: L.divIcon({
className: 'add-visit-marker', className: "add-visit-marker",
html: '📍', html: "📍",
iconSize: [30, 30], iconSize: [30, 30],
iconAnchor: [15, 15] iconAnchor: [15, 15],
}) }),
}).addTo(this.map); }).addTo(this.map);
// Show the visit form popup // Show the visit form popup
@ -202,11 +212,14 @@ export default class extends Controller {
showVisitForm(lat, lng) { showVisitForm(lat, lng) {
// Get current date/time for default values // Get current date/time for default values
const now = new Date(); const now = new Date();
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000)); const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
// Format dates for datetime-local input // Format dates for datetime-local input (2011-10-05T14:48) (YYYY-MM-DDTHH:mm)
const formatDateTime = (date) => { const formatDateTime = (date) => {
return date.toISOString().slice(0, 16); 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 startTime = formatDateTime(now);
@ -257,7 +270,7 @@ export default class extends Controller {
closeOnClick: false, closeOnClick: false,
autoClose: false, autoClose: false,
maxWidth: 300, maxWidth: 300,
className: 'visit-form-popup' className: "visit-form-popup",
}) })
.setLatLng([lat, lng]) .setLatLng([lat, lng])
.setContent(formHTML) .setContent(formHTML)
@ -265,16 +278,16 @@ export default class extends Controller {
// Add event listeners after the popup is added to DOM // Add event listeners after the popup is added to DOM
setTimeout(() => { setTimeout(() => {
const form = document.getElementById('add-visit-form'); const form = document.getElementById("add-visit-form");
const cancelButton = document.getElementById('cancel-visit'); const cancelButton = document.getElementById("cancel-visit");
const nameInput = document.getElementById('visit-name'); const nameInput = document.getElementById("visit-name");
if (form) { if (form) {
form.addEventListener('submit', (e) => this.handleFormSubmit(e)); form.addEventListener("submit", (e) => this.handleFormSubmit(e));
} }
if (cancelButton) { if (cancelButton) {
cancelButton.addEventListener('click', () => { cancelButton.addEventListener("click", () => {
this.exitAddVisitMode(this.addVisitButton); this.exitAddVisitMode(this.addVisitButton);
}); });
} }
@ -292,15 +305,21 @@ export default class extends Controller {
const form = event.target; const form = event.target;
const formData = new FormData(form); 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 // Get form values
const visitData = { const visitData = {
visit: { visit: {
name: formData.get('name'), name: formData.get("name"),
started_at: formData.get('started_at'), started_at: localToUTCISOString(formData.get("started_at")),
ended_at: formData.get('ended_at'), ended_at: localToUTCISOString(formData.get("ended_at")),
latitude: formData.get('latitude'), latitude: formData.get("latitude"),
longitude: formData.get('longitude') longitude: formData.get("longitude"),
} },
}; };
// Validate that end time is after start time // Validate that end time is after start time
@ -308,7 +327,7 @@ export default class extends Controller {
const endTime = new Date(visitData.visit.ended_at); const endTime = new Date(visitData.visit.ended_at);
if (endTime <= startTime) { if (endTime <= startTime) {
showFlashMessage('error', 'End time must be after start time'); showFlashMessage("error", "End time must be after start time");
return; return;
} }
@ -316,23 +335,26 @@ export default class extends Controller {
const submitButton = form.querySelector('button[type="submit"]'); const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent; const originalText = submitButton.textContent;
submitButton.disabled = true; submitButton.disabled = true;
submitButton.textContent = 'Creating...'; submitButton.textContent = "Creating...";
try { try {
const response = await fetch(`/api/v1/visits`, { const response = await fetch(`/api/v1/visits`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'Accept': 'application/json', Accept: "application/json",
'Authorization': `Bearer ${this.apiKeyValue}` Authorization: `Bearer ${this.apiKeyValue}`,
}, },
body: JSON.stringify(visitData) body: JSON.stringify(visitData),
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
showFlashMessage('notice', `Visit "${visitData.visit.name}" created successfully!`); showFlashMessage(
"notice",
`Visit "${visitData.visit.name}" created successfully!`
);
this.exitAddVisitMode(this.addVisitButton); this.exitAddVisitMode(this.addVisitButton);
// Refresh visits layer - this will clear and refetch data // Refresh visits layer - this will clear and refetch data
@ -343,12 +365,13 @@ export default class extends Controller {
this.ensureVisitsLayersEnabled(); this.ensureVisitsLayersEnabled();
}, 300); }, 300);
} else { } else {
const errorMessage = data.error || data.message || 'Failed to create visit'; const errorMessage =
showFlashMessage('error', errorMessage); data.error || data.message || "Failed to create visit";
showFlashMessage("error", errorMessage);
} }
} catch (error) { } catch (error) {
console.error('Error creating visit:', error); console.error("Error creating visit:", error);
showFlashMessage('error', 'Network error: Failed to create visit'); showFlashMessage("error", "Network error: Failed to create visit");
} finally { } finally {
// Re-enable form // Re-enable form
submitButton.disabled = false; submitButton.disabled = false;
@ -357,16 +380,20 @@ export default class extends Controller {
} }
refreshVisitsLayer() { refreshVisitsLayer() {
console.log('Attempting to refresh visits layer...'); console.log("Attempting to refresh visits layer...");
// Try multiple approaches to refresh the visits layer // Try multiple approaches to refresh the visits layer
const mapsController = document.querySelector('[data-controller*="maps"]'); const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) { if (mapsController) {
// Try to get the Stimulus controller instance // Try to get the Stimulus controller instance
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps'); const stimulusController =
this.application.getControllerForElementAndIdentifier(
mapsController,
"maps"
);
if (stimulusController && stimulusController.visitsManager) { if (stimulusController && stimulusController.visitsManager) {
console.log('Found maps controller with visits manager'); console.log("Found maps controller with visits manager");
// Clear existing visits and fetch fresh data // Clear existing visits and fetch fresh data
if (stimulusController.visitsManager.visitCircles) { if (stimulusController.visitsManager.visitCircles) {
@ -377,48 +404,62 @@ export default class extends Controller {
} }
// Refresh the visits data // Refresh the visits data
if (typeof stimulusController.visitsManager.fetchAndDisplayVisits === 'function') { if (
console.log('Refreshing visits data...'); typeof stimulusController.visitsManager.fetchAndDisplayVisits ===
"function"
) {
console.log("Refreshing visits data...");
stimulusController.visitsManager.fetchAndDisplayVisits(); stimulusController.visitsManager.fetchAndDisplayVisits();
} }
} else { } else {
console.log('Could not find maps controller or visits manager'); console.log("Could not find maps controller or visits manager");
// Fallback: Try to dispatch a custom event // Fallback: Try to dispatch a custom event
const refreshEvent = new CustomEvent('visits:refresh', { bubbles: true }); const refreshEvent = new CustomEvent("visits:refresh", {
bubbles: true,
});
mapsController.dispatchEvent(refreshEvent); mapsController.dispatchEvent(refreshEvent);
} }
} else { } else {
console.log('Could not find maps controller element'); console.log("Could not find maps controller element");
} }
} }
ensureVisitsLayersEnabled() { ensureVisitsLayersEnabled() {
console.log('Ensuring visits layers are enabled...'); console.log("Ensuring visits layers are enabled...");
const mapsController = document.querySelector('[data-controller*="maps"]'); const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) { if (mapsController) {
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps'); const stimulusController =
this.application.getControllerForElementAndIdentifier(
mapsController,
"maps"
);
if (stimulusController && stimulusController.map && stimulusController.visitsManager) { if (
stimulusController &&
stimulusController.map &&
stimulusController.visitsManager
) {
const map = stimulusController.map; const map = stimulusController.map;
const visitsManager = stimulusController.visitsManager; const visitsManager = stimulusController.visitsManager;
// Get the confirmed visits layer (newly created visits are always confirmed) // Get the confirmed visits layer (newly created visits are always confirmed)
const confirmedVisitsLayer = visitsManager.getConfirmedVisitCirclesLayer(); const confirmedVisitsLayer =
visitsManager.getConfirmedVisitCirclesLayer();
// Ensure confirmed visits layer is added to map since we create confirmed visits // Ensure confirmed visits layer is added to map since we create confirmed visits
if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) { if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) {
console.log('Adding confirmed visits layer to map'); console.log("Adding confirmed visits layer to map");
map.addLayer(confirmedVisitsLayer); map.addLayer(confirmedVisitsLayer);
// Update the layer control checkbox to reflect the layer is now active // Update the layer control checkbox to reflect the layer is now active
this.updateLayerControlCheckbox('Confirmed Visits', true); this.updateLayerControlCheckbox("Confirmed Visits", true);
} }
// Refresh visits data to include the new visit // Refresh visits data to include the new visit
if (typeof visitsManager.fetchAndDisplayVisits === 'function') { if (typeof visitsManager.fetchAndDisplayVisits === "function") {
console.log('Final refresh of visits to show new visit...'); console.log("Final refresh of visits to show new visit...");
visitsManager.fetchAndDisplayVisits(); visitsManager.fetchAndDisplayVisits();
} }
} }
@ -427,28 +468,32 @@ export default class extends Controller {
updateLayerControlCheckbox(layerName, isEnabled) { updateLayerControlCheckbox(layerName, isEnabled) {
// Find the layer control input for the specified layer // Find the layer control input for the specified layer
const layerControlContainer = document.querySelector('.leaflet-control-layers'); const layerControlContainer = document.querySelector(
".leaflet-control-layers"
);
if (!layerControlContainer) { if (!layerControlContainer) {
console.log('Layer control container not found'); console.log("Layer control container not found");
return; return;
} }
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]'); const inputs = layerControlContainer.querySelectorAll(
inputs.forEach(input => { 'input[type="checkbox"]'
);
inputs.forEach((input) => {
const label = input.nextElementSibling; const label = input.nextElementSibling;
if (label && label.textContent.trim() === layerName) { if (label && label.textContent.trim() === layerName) {
console.log(`Updating ${layerName} checkbox to ${isEnabled}`); console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
input.checked = isEnabled; input.checked = isEnabled;
// Trigger change event to ensure proper state management // Trigger change event to ensure proper state management
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true }));
} }
}); });
} }
cleanup() { cleanup() {
if (this.map) { if (this.map) {
this.map.off('click', this.onMapClick, this); this.map.off("click", this.onMapClick, this);
if (this.addVisitMarker) { if (this.addVisitMarker) {
this.map.removeLayer(this.addVisitMarker); this.map.removeLayer(this.addVisitMarker);