import React, { useState, useEffect, useMemo, useCallback } from 'react'; import SearchBar from '../components/SearchBar'; import MedicineResults from '../components/MedicineResults'; import ProductResults from '../components/ProductResults'; import PharmacyList from '../components/PharmacyList'; import PharmacyMap from '../components/PharmacyMap'; import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo'; import { useTranslation } from '../i18n'; import { getOpenStatus } from '../utils/hours'; import './SearchView.css'; const suggestions = [ { name: 'Paracetamol', icon: 'medication', color: 'neutral-1' }, { name: 'Ibuprofeno', icon: 'pill', color: 'neutral-2' }, { name: 'Aspirina', icon: 'vaccines', color: 'neutral-3' }, { name: 'Omeprazol', icon: 'emergency_home', color: 'neutral-4' }, ]; function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigateToProduct }) { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(initialQuery); const [medicines, setMedicines] = useState([]); const [products, setProducts] = useState([]); const [selectedMedicine, setSelectedMedicine] = useState(null); const [pharmacies, setPharmacies] = useState([]); const [loading, setLoading] = useState(false); const [userPosition, setUserPosition] = useState(null); const [positionSource, setPositionSource] = useState(null); const [sortByDistance, setSortByDistance] = useState(false); const [openNow, setOpenNow] = useState(false); const [locating, setLocating] = useState(false); const [locationError, setLocationError] = useState(''); const [recentSearches, setRecentSearches] = useState([]); // Precache position on mount — makes first "sort by distance" nearly instant useEffect(() => { if (hasCachedPosition()) { getUserPosition().then(pos => { setUserPosition(pos); }).catch(() => {}); } }, []); // Fetch recent searches when user is logged in useEffect(() => { if (!currentUser) { setRecentSearches([]); return; } fetch('/api/search/recent', { credentials: 'include' }) .then((r) => r.json()) .then((data) => setRecentSearches(Array.isArray(data) ? data : [])) .catch(() => setRecentSearches([])); }, [currentUser]); const saveToRecent = useCallback((medicine) => { if (!currentUser) return; fetch('/api/search/recent', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ medicine }), }).catch(() => {}); // Update local state immediately setRecentSearches((prev) => { const filtered = prev.filter((m) => m.id !== medicine.id); return [ { id: medicine.id, name: medicine.name, active_ingredient: medicine.active_ingredient, dosage: medicine.dosage, form: medicine.form, timestamp: Date.now(), }, ...filtered, ].slice(0, 5); }); }, [currentUser]); useEffect(() => { const searchAll = async () => { if (searchQuery.trim().length < 2) { setMedicines([]); setProducts([]); setSelectedMedicine(null); setPharmacies([]); return; } setLoading(true); const query = searchQuery.trim(); try { // Search both CIMA and Parapharmacy simultaneously const [medicinesRes, productsRes] = await Promise.allSettled([ fetch(`/api/medicines/search?q=${encodeURIComponent(query)}`), fetch(`/api/products/parapharmacy/search?q=${encodeURIComponent(query)}`) ]); // Only update if this search is still the current one if (query !== searchQuery.trim()) return; if (medicinesRes.status === 'fulfilled' && medicinesRes.value.ok) { const medicinesData = await medicinesRes.value.json(); setMedicines(medicinesData); } if (productsRes.status === 'fulfilled' && productsRes.value.ok) { const productsData = await productsRes.value.json(); setProducts(productsData.results || []); } } catch (error) { console.error('Search error:', error); } finally { setLoading(false); } }; const timeoutId = setTimeout(searchAll, 300); return () => clearTimeout(timeoutId); }, [searchQuery]); useEffect(() => { const fetchPharmacies = async () => { if (!selectedMedicine) { setPharmacies([]); return; } setLoading(true); try { const response = await fetch(`/api/medicines/${selectedMedicine.id}/pharmacies`); const data = await response.json(); setPharmacies(data); } catch (error) { console.error('Error fetching pharmacies:', error); } finally { setLoading(false); } }; fetchPharmacies(); }, [selectedMedicine]); const savedLat = currentUser?.latitude; const savedLon = currentUser?.longitude; const hasSavedCoords = savedLat != null && savedLon != null; const handleSortByDistance = async () => { if (sortByDistance) { setSortByDistance(false); return; } setLocationError(''); if (hasSavedCoords) { setUserPosition({ lat: savedLat, lon: savedLon }); setPositionSource('profile'); setSortByDistance(true); // Save to search history try { fetch('/api/search-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ address: currentUser?.address, latitude: savedLat, longitude: savedLon, }), }); } catch (err) { console.error('Error saving search history:', err); } return; } if (userPosition) { setSortByDistance(true); setPositionSource('cached'); return; } setLocating(true); try { const pos = await getUserPosition(); setUserPosition(pos); setPositionSource('browser'); setSortByDistance(true); // Save to search history try { fetch('/api/search-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ address: 'Ubicación actual', latitude: pos.lat, longitude: pos.lon, }), }); } catch (err) { console.error('Error saving search history:', err); } } catch (err) { let msg = t('search.locationError'); if (err && typeof err.code === 'number') { if (err.code === 1) msg = t('search.locationDenied'); else if (err.code === 2) msg = t('search.locationUnavailable'); else if (err.code === 3) msg = t('search.locationTimeout'); } setLocationError(msg); } finally { setLocating(false); } }; const displayedPharmacies = useMemo(() => { let result = pharmacies; if (openNow) { result = result.filter((p) => { if (p.is_open === true) return true; if (p.is_open == null) { const s = getOpenStatus(p.opening_hours); return s && s.status === 'open'; } return false; }); } if (!sortByDistance || !userPosition) return result; return [...result].sort((a, b) => { if (a.latitude == null || a.longitude == null) return 1; if (b.latitude == null || b.longitude == null) return -1; return ( haversineKm(userPosition.lat, userPosition.lon, a.latitude, a.longitude) - haversineKm(userPosition.lat, userPosition.lon, b.latitude, b.longitude) ); }); }, [pharmacies, sortByDistance, userPosition, openNow]); return (
{loading &&
{t('search.searching')}
} {!searchQuery && !selectedMedicine && ( <>

{t('search.suggestions')}

{suggestions.map((s, i) => ( ))}
{currentUser && recentSearches.length > 0 && (

{t('search.recentResults')}

{recentSearches.map((r) => (
setSelectedMedicine(r)} style={{ cursor: 'pointer' }} >

{r.name}

{r.dosage && `${r.dosage}`} {r.dosage && r.form && ' \u2022 '} {r.form}

{r.active_ingredient && ( {r.active_ingredient} )}
))}
)} )} {searchQuery && !selectedMedicine && ( <>
{(medicines.length + products.length) > 0 && ( {medicines.length + products.length} {t('search.resultsFound')} )}
{medicines.length > 0 && ( { saveToRecent(m); setSelectedMedicine(m); }} query={searchQuery} currentUser={currentUser} onLoginRequest={onLoginRequest} /> )} {products.length > 0 && (

{t('search.parapharmacy')}

{ const productId = p._id || p.id; if (onNavigateToProduct) { onNavigateToProduct(p.source, productId); } else { window.location.href = `/product/${p.source}/${productId}`; } }} />
)} )} {selectedMedicine && (

{selectedMedicine.name}

{t('search.activeIngredient')} {selectedMedicine.active_ingredient} {t('search.dosage')} {selectedMedicine.dosage} {t('search.form')} {selectedMedicine.form}
{pharmacies.length > 0 && (
{sortByDistance && positionSource === 'profile' && ( {t('search.usingSavedAddress')} )} {sortByDistance && positionSource === 'cached' && ( {t('search.usingRecentLocation')} )} {locationError && ( {locationError} )}
)} {openNow && pharmacies.length > 0 && displayedPharmacies.length === 0 && (
{t('pharmacy.filterNoResults')}
)}
)}
); } export default SearchView;