4404db62ee
- Added avatar selection popup with predefined avatar options - Redesigned ProfileView (web + mobile) with avatar upload and editable fields - Added AvatarSelectionModal for mobile profile - Fixed medicine search: parse dosage terms (e.g. 'Paracetamol 1G') to query CIMA only by name and filter by dosage field - Updated styles for profile, search, and medicine results
342 lines
12 KiB
React
342 lines
12 KiB
React
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
import SearchBar from '../components/SearchBar';
|
|
import MedicineResults from '../components/MedicineResults';
|
|
import PharmacyList from '../components/PharmacyList';
|
|
import PharmacyMap from '../components/PharmacyMap';
|
|
import { haversineKm, getUserPosition } from '../utils/geo';
|
|
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 = '' }) {
|
|
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
|
const [medicines, setMedicines] = 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 [locating, setLocating] = useState(false);
|
|
const [locationError, setLocationError] = useState('');
|
|
const [recentSearches, setRecentSearches] = useState([]);
|
|
|
|
// 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, 10);
|
|
});
|
|
}, [currentUser]);
|
|
|
|
useEffect(() => {
|
|
const searchMedicines = async () => {
|
|
if (searchQuery.trim().length < 2) {
|
|
setMedicines([]);
|
|
setSelectedMedicine(null);
|
|
setPharmacies([]);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/medicines/search?q=${encodeURIComponent(searchQuery)}`);
|
|
const data = await response.json();
|
|
setMedicines(data);
|
|
} catch (error) {
|
|
console.error('Error searching medicines:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
const timeoutId = setTimeout(searchMedicines, 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);
|
|
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 = 'No se pudo obtener tu ubicación';
|
|
if (err && typeof err.code === 'number') {
|
|
if (err.code === 1) msg = 'Permiso de ubicación denegado';
|
|
else if (err.code === 2) msg = 'Ubicación no disponible';
|
|
else if (err.code === 3) msg = 'La solicitud de ubicación expiró';
|
|
}
|
|
setLocationError(msg);
|
|
} finally {
|
|
setLocating(false);
|
|
}
|
|
};
|
|
|
|
const displayedPharmacies = useMemo(() => {
|
|
if (!sortByDistance || !userPosition) return pharmacies;
|
|
return [...pharmacies].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]);
|
|
|
|
return (
|
|
<div className="search-view">
|
|
<div className="search-content">
|
|
<SearchBar
|
|
value={searchQuery}
|
|
onChange={setSearchQuery}
|
|
placeholder="Escriba el nombre del medicamento"
|
|
/>
|
|
|
|
{loading && <div className="loading">Buscando...</div>}
|
|
|
|
{!searchQuery && !selectedMedicine && (
|
|
<>
|
|
<section className="suggestions-section">
|
|
<h2 className="section-title">Sugerencias</h2>
|
|
<div className="suggestions-grid">
|
|
{suggestions.map((s, i) => (
|
|
<button
|
|
key={i}
|
|
className={`suggestion-btn suggestion-btn--${s.color}`}
|
|
onClick={() => setSearchQuery(s.name)}
|
|
>
|
|
<div className="suggestion-icon">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z" />
|
|
</svg>
|
|
</div>
|
|
<span className="suggestion-name">{s.name}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{currentUser && recentSearches.length > 0 && (
|
|
<section className="recent-section">
|
|
<h2 className="section-title">Resultados Recientes</h2>
|
|
<div className="recent-list">
|
|
{recentSearches.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="recent-card"
|
|
onClick={() => setSelectedMedicine(r)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
<div className="recent-card-top">
|
|
<div>
|
|
<h3 className="recent-name">{r.name}</h3>
|
|
<p className="recent-detail">
|
|
{r.dosage && `${r.dosage}`}
|
|
{r.dosage && r.form && ' \u2022 '}
|
|
{r.form}
|
|
</p>
|
|
</div>
|
|
{r.active_ingredient && (
|
|
<span className="recent-tag">{r.active_ingredient}</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
className="recent-btn"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedMedicine(r);
|
|
}}
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="10" />
|
|
<polyline points="12 6 12 12 16 14" />
|
|
</svg>
|
|
Encontrar cerca
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{searchQuery && !selectedMedicine && (
|
|
<MedicineResults
|
|
medicines={medicines}
|
|
onSelect={(m) => {
|
|
saveToRecent(m);
|
|
setSelectedMedicine(m);
|
|
}}
|
|
query={searchQuery}
|
|
currentUser={currentUser}
|
|
onLoginRequest={onLoginRequest}
|
|
/>
|
|
)}
|
|
|
|
{selectedMedicine && (
|
|
<div className="selected-medicine-section">
|
|
<div className="medicine-info">
|
|
<h2>{selectedMedicine.name}</h2>
|
|
<div className="medicine-details">
|
|
<span><strong>Ingrediente Activo:</strong> {selectedMedicine.active_ingredient}</span>
|
|
<span><strong>Dosis:</strong> {selectedMedicine.dosage}</span>
|
|
<span><strong>Forma:</strong> {selectedMedicine.form}</span>
|
|
</div>
|
|
<button
|
|
className="back-button"
|
|
onClick={() => {
|
|
setSelectedMedicine(null);
|
|
setPharmacies([]);
|
|
}}
|
|
>
|
|
← Volver a búsqueda
|
|
</button>
|
|
</div>
|
|
|
|
{pharmacies.length > 0 && (
|
|
<div className="pharmacy-controls">
|
|
<button
|
|
className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
|
|
onClick={handleSortByDistance}
|
|
disabled={locating}
|
|
>
|
|
{locating
|
|
? '📍 Localizando…'
|
|
: sortByDistance
|
|
? '📍 Ordenado por distancia · Reset'
|
|
: hasSavedCoords
|
|
? '📍 Ordenar por ubicación guardada'
|
|
: '📍 Ordenar por distancia'}
|
|
</button>
|
|
{sortByDistance && positionSource === 'profile' && (
|
|
<span className="location-source">Usando tu dirección guardada</span>
|
|
)}
|
|
{locationError && (
|
|
<span className="location-error">{locationError}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<PharmacyMap pharmacies={displayedPharmacies} />
|
|
<PharmacyList
|
|
pharmacies={displayedPharmacies}
|
|
loading={loading}
|
|
userPosition={sortByDistance ? userPosition : null}
|
|
medicine={selectedMedicine}
|
|
currentUser={currentUser}
|
|
onLoginRequest={onLoginRequest}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SearchView;
|