`;
}).join('');
container.innerHTML = html;
// Add the circles layer to the map
this.visitCircles.addTo(this.map);
// Add click handlers to visit items and buttons
this.addVisitItemEventListeners(container);
}
/**
* Adds event listeners to visit items in the drawer
* @param {HTMLElement} container - The container element with visit items
*/
addVisitItemEventListeners(container) {
const visitItems = container.querySelectorAll('.visit-item');
visitItems.forEach(item => {
// Location click handler
item.addEventListener('click', (event) => {
// Don't trigger if clicking on buttons
if (event.target.classList.contains('btn')) return;
const lat = parseFloat(item.dataset.lat);
const lng = parseFloat(item.dataset.lng);
if (!isNaN(lat) && !isNaN(lng)) {
this.map.setView([lat, lng], 15, {
animate: true,
duration: 1
});
}
});
// Confirm button handler
const confirmBtn = item.querySelector('.confirm-visit');
confirmBtn?.addEventListener('click', async (event) => {
event.stopPropagation();
const visitId = event.target.dataset.id;
try {
const response = await fetch(`/api/v1/visits/${visitId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
visit: {
status: 'confirmed'
}
})
});
if (!response.ok) throw new Error('Failed to confirm visit');
// Refresh visits list
this.fetchAndDisplayVisits();
showFlashMessage('notice', 'Visit confirmed successfully');
} catch (error) {
console.error('Error confirming visit:', error);
showFlashMessage('error', 'Failed to confirm visit');
}
});
// Decline button handler
const declineBtn = item.querySelector('.decline-visit');
declineBtn?.addEventListener('click', async (event) => {
event.stopPropagation();
const visitId = event.target.dataset.id;
try {
const response = await fetch(`/api/v1/visits/${visitId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
visit: {
status: 'declined'
}
})
});
if (!response.ok) throw new Error('Failed to decline visit');
// Refresh visits list
this.fetchAndDisplayVisits();
showFlashMessage('notice', 'Visit declined successfully');
} catch (error) {
console.error('Error declining visit:', error);
showFlashMessage('error', 'Failed to decline visit');
}
});
});
}
/**
* Fetches possible places for a visit and displays them in a popup
* @param {Object} visit - The visit object
*/
async fetchPossiblePlaces(visit) {
try {
// Close any existing popup before opening a new one
if (this.currentPopup) {
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
}
const response = await fetch(`/api/v1/visits/${visit.id}/possible_places`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
}
});
if (!response.ok) throw new Error('Failed to fetch possible places');
const possiblePlaces = await response.json();
// Create popup content with form and dropdown
const defaultName = visit.name;
const popupContent = `
`;
// Create and store the popup
const popup = L.popup({
closeButton: true,
closeOnClick: false,
autoClose: false,
maxWidth: 300, // Set maximum width
minWidth: 200 // Set minimum width
})
.setLatLng([visit.place.latitude, visit.place.longitude])
.setContent(popupContent);
// Store the current popup
this.currentPopup = popup;
// Open the popup
popup.openOn(this.map);
// Add form submit handler
this.addPopupFormEventListeners(visit);
} catch (error) {
console.error('Error fetching possible places:', error);
showFlashMessage('error', 'Failed to load possible places');
}
}
/**
* Adds event listeners to the popup form
* @param {Object} visit - The visit object
*/
addPopupFormEventListeners(visit) {
const form = document.querySelector(`.visit-name-form[data-visit-id="${visit.id}"]`);
if (form) {
form.addEventListener('submit', async (event) => {
event.preventDefault(); // Prevent form submission
event.stopPropagation(); // Stop event bubbling
const newName = event.target.querySelector('input').value;
const selectedPlaceId = event.target.querySelector('select[name="place"]').value;
// Get the selected place name from the dropdown
const selectedOption = event.target.querySelector(`select[name="place"] option[value="${selectedPlaceId}"]`);
const selectedPlaceName = selectedOption ? selectedOption.textContent.trim() : '';
console.log('Selected new place:', selectedPlaceName);
try {
const response = await fetch(`/api/v1/visits/${visit.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
visit: {
name: newName,
place_id: selectedPlaceId
}
})
});
if (!response.ok) throw new Error('Failed to update visit');
// Get the updated visit data from the response
const updatedVisit = await response.json();
// Update the local visit object with the latest data
// This ensures that if the popup is opened again, it will show the updated values
visit.name = updatedVisit.name || newName;
visit.place = updatedVisit.place;
// Use the selected place name for the update
const updatedName = selectedPlaceName || newName;
console.log('Updating visit name in drawer to:', updatedName);
// Update the visit name in the drawer panel
const drawerVisitItem = document.querySelector(`.drawer .visit-item[data-id="${visit.id}"]`);
if (drawerVisitItem) {
const nameElement = drawerVisitItem.querySelector('.font-semibold');
if (nameElement) {
console.log('Previous name in drawer:', nameElement.textContent);
nameElement.textContent = updatedName;
// Add a highlight effect to make the change visible
nameElement.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
setTimeout(() => {
nameElement.style.backgroundColor = '';
}, 2000);
console.log('Updated name in drawer to:', nameElement.textContent);
}
}
// Close the popup
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
showFlashMessage('notice', 'Visit updated successfully');
} catch (error) {
console.error('Error updating visit:', error);
showFlashMessage('error', 'Failed to update visit');
}
});
// Add event listeners for confirm and decline buttons
const confirmBtn = form.querySelector('.confirm-visit');
const declineBtn = form.querySelector('.decline-visit');
confirmBtn?.addEventListener('click', (event) => this.handleStatusChange(event, visit.id, 'confirmed'));
declineBtn?.addEventListener('click', (event) => this.handleStatusChange(event, visit.id, 'declined'));
}
}
/**
* Handles status change for a visit (confirm/decline)
* @param {Event} event - The click event
* @param {string} visitId - The visit ID
* @param {string} status - The new status ('confirmed' or 'declined')
*/
async handleStatusChange(event, visitId, status) {
event.preventDefault();
event.stopPropagation();
try {
const response = await fetch(`/api/v1/visits/${visitId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
visit: {
status: status
}
})
});
if (!response.ok) throw new Error(`Failed to ${status} visit`);
if (this.currentPopup) {
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
}
this.fetchAndDisplayVisits();
showFlashMessage('notice', `Visit ${status}d successfully`);
} catch (error) {
console.error(`Error ${status}ing visit:`, error);
showFlashMessage('error', `Failed to ${status} visit`);
}
}
/**
* Truncates text to a specified length and adds ellipsis if needed
* @param {string} text - The text to truncate
* @param {number} maxLength - The maximum length
* @returns {string} Truncated text
*/
truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
/**
* Gets the visits layer group for adding to the map controls
* @returns {L.LayerGroup} The visits layer group
*/
getVisitCirclesLayer() {
return this.visitCircles;
}
}