freealberta/map/app/public/js/database-search.js

313 lines
9.5 KiB
JavaScript

/**
* Database Search Module
* Handles location search functionality through loaded map data
*/
import { map } from './map-manager.js';
import { markers } from './location-manager.js';
import { openEditForm } from './location-manager.js';
export class DatabaseSearch {
constructor() {
this.searchCache = new Map();
}
/**
* Search through loaded locations
* @param {string} query - The search query
* @returns {Promise<Array>} Array of search results
*/
async search(query) {
if (!query || query.trim().length < 2) {
return [];
}
const trimmedQuery = query.trim().toLowerCase();
// Check cache first
if (this.searchCache.has(trimmedQuery)) {
return this.searchCache.get(trimmedQuery);
}
try {
// Get all locations from loaded markers
const locations = this.getLoadedLocations();
// Filter locations based on search query
const results = locations.filter(location => {
return this.matchesQuery(location, trimmedQuery);
}).map(location => {
return this.formatResult(location, trimmedQuery);
}).slice(0, 10); // Limit to 10 results
// Cache the results
this.searchCache.set(trimmedQuery, results);
// Clean up cache if it gets too large
if (this.searchCache.size > 50) {
const firstKey = this.searchCache.keys().next().value;
this.searchCache.delete(firstKey);
}
return results;
} catch (error) {
console.error('Database search error:', error);
throw error;
}
}
/**
* Get all loaded location data from markers
* @returns {Array} Array of location objects
*/
getLoadedLocations() {
const locations = [];
markers.forEach(marker => {
if (marker._locationData) {
locations.push(marker._locationData);
}
});
return locations;
}
/**
* Check if a location matches the search query
* @param {Object} location - Location object
* @param {string} query - Search query (lowercase)
* @returns {boolean} Whether the location matches
*/
matchesQuery(location, query) {
const searchFields = [
location['First Name'],
location['Last Name'],
location.Email,
location.Phone,
location.Address,
location['Unit Number'],
location.Notes
];
// Combine first and last name
const fullName = [location['First Name'], location['Last Name']]
.filter(Boolean).join(' ').toLowerCase();
return searchFields.some(field => {
if (!field) return false;
return String(field).toLowerCase().includes(query);
}) || fullName.includes(query);
}
/**
* Format a location for search results
* @param {Object} location - Location object
* @param {string} query - Search query for highlighting
* @returns {Object} Formatted result
*/
formatResult(location, query) {
const name = [location['First Name'], location['Last Name']]
.filter(Boolean).join(' ') || 'Unknown';
const address = location.Address || 'No address';
const email = location.Email || '';
const phone = location.Phone || '';
const unit = location['Unit Number'] || '';
const supportLevel = location['Support Level'] || '';
const notes = location.Notes || '';
// Create a snippet with highlighted matches
const snippet = this.createSnippet(location, query);
return {
id: location.Id || location.id || location.ID || location._id,
name,
address,
email,
phone,
unit,
supportLevel,
notes,
snippet,
coordinates: {
lat: parseFloat(location.latitude) || 0,
lng: parseFloat(location.longitude) || 0
},
location: location // Keep full location data for actions
};
}
/**
* Create a text snippet with highlighted matches
* @param {Object} location - Location object
* @param {string} query - Search query
* @returns {string} Snippet with highlights
*/
createSnippet(location, query) {
const searchableText = [
location['First Name'],
location['Last Name'],
location.Email,
location.Address,
location['Unit Number'],
location.Notes
].filter(Boolean).join(' • ');
if (searchableText.length <= 100) {
return this.highlightQuery(searchableText, query);
}
// Find the first occurrence of the query
const lowerText = searchableText.toLowerCase();
const index = lowerText.indexOf(query);
if (index === -1) {
// Return first 100 characters if no match
return searchableText.substring(0, 100) + (searchableText.length > 100 ? '...' : '');
}
// Extract snippet around the match
const start = Math.max(0, index - 30);
const end = Math.min(searchableText.length, start + 100);
let snippet = searchableText.substring(start, end);
// Add ellipsis if needed
if (start > 0) snippet = '...' + snippet;
if (end < searchableText.length) snippet = snippet + '...';
return this.highlightQuery(snippet, query);
}
/**
* Highlight search query in text
* @param {string} text - Text to highlight
* @param {string} query - Query to highlight
* @returns {string} Text with highlights
*/
highlightQuery(text, query) {
if (!query || !text) return text;
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
/**
* Create HTML for a search result
* @param {Object} result - Search result object
* @returns {HTMLElement} Result element
*/
createResultElement(result) {
const resultEl = document.createElement('div');
resultEl.className = 'search-result-item search-result-database';
const supportLevelText = result.supportLevel ? `Level ${result.supportLevel}` : '';
const unitText = result.unit ? `Unit ${result.unit}` : '';
resultEl.innerHTML = `
<div class="result-name">${this.escapeHtml(result.name)}</div>
<div class="result-address">${this.escapeHtml(result.address)} ${this.escapeHtml(unitText)}</div>
<div class="result-snippet">${result.snippet}</div>
<div class="result-details">
${result.email ? `📧 ${this.escapeHtml(result.email)}` : ''}
${result.phone ? ` 📞 ${this.escapeHtml(result.phone)}` : ''}
${supportLevelText ? ` 🎯 ${supportLevelText}` : ''}
</div>
`;
resultEl.addEventListener('click', () => {
this.selectResult(result);
});
return resultEl;
}
/**
* Handle selection of a search result
* @param {Object} result - Selected result
*/
selectResult(result) {
if (!map) {
console.error('Map not available');
return;
}
const { lat, lng } = result.coordinates;
if (isNaN(lat) || isNaN(lng)) {
console.error('Invalid coordinates in result:', result);
return;
}
// Pan and zoom to the location
map.setView([lat, lng], 17);
// Find and open the marker popup
const marker = markers.find(m => {
if (!m._locationData) return false;
const markerId = m._locationData.Id || m._locationData.id || m._locationData.ID || m._locationData._id;
return markerId == result.id;
});
if (marker) {
// Open the popup
marker.openPopup();
// Optionally highlight the marker temporarily
this.highlightMarker(marker);
}
}
/**
* Temporarily highlight a marker
* @param {Object} marker - Leaflet marker
*/
highlightMarker(marker) {
if (!marker || !marker.setStyle) return;
const originalStyle = {
fillColor: marker.options.fillColor,
color: marker.options.color,
weight: marker.options.weight,
radius: marker.options.radius
};
// Highlight style
marker.setStyle({
fillColor: '#FFD700',
color: '#FF6B35',
weight: 4,
radius: 12
});
// Restore original style after 3 seconds
setTimeout(() => {
marker.setStyle(originalStyle);
}, 3000);
}
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
/**
* Clear the search cache
*/
clearCache() {
this.searchCache.clear();
}
}
// Create a global instance
window.databaseSearchInstance = new DatabaseSearch();
export default window.databaseSearchInstance;