/** * 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 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, '$1'); } /** * 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 = `
${this.escapeHtml(result.name)}
${this.escapeHtml(result.address)} ${this.escapeHtml(unitText)}
${result.snippet}
${result.email ? `📧 ${this.escapeHtml(result.email)}` : ''} ${result.phone ? ` 📞 ${this.escapeHtml(result.phone)}` : ''} ${supportLevelText ? ` 🎯 ${supportLevelText}` : ''}
`; 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;