Files
FarmaFinder/apps/frontend/src/views/PublicView.jsx
T
2026-07-06 18:47:43 +02:00

293 lines
9.5 KiB
React

import React, { useState, useEffect, useMemo } from 'react';
import '../App.css';
import SearchBar from '../components/SearchBar';
import MedicineResults from '../components/MedicineResults';
import PharmacyList from '../components/PharmacyList';
import PharmacyMap from '../components/PharmacyMap';
import HomeView from './HomeView';
import ScannerView from './ScannerView';
import { haversineKm, getUserPosition } from '../utils/geo';
function PublicView({
currentUser,
onLoginRequest,
screen: screenProp,
onScreenChange,
}) {
// ponytail: uncontrolled by default (own state); controlled when App.jsx passes props
// so BottomNav can drive the inner screen directly. No-op fallback breaks tests.
const [internalScreen, setInternalScreen] = useState('home');
const screen = screenProp ?? internalScreen;
const setScreen = onScreenChange ?? setInternalScreen;
const [searchQuery, setSearchQuery] = useState('');
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); // 'profile' | 'browser'
const [sortByDistance, setSortByDistance] = useState(false);
const [locating, setLocating] = useState(false);
const [locationError, setLocationError] = useState('');
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) {
console.error('[location] error', err);
let msg;
if (err && typeof err.code === 'number') {
if (err.code === 1) msg = 'Permiso de ubicación denegado — actívalo en la configuración del navegador';
else if (err.code === 2) msg = 'Ubicación no disponible — comprueba los servicios de ubicación/WiFi';
else if (err.code === 3) msg = 'La solicitud de ubicación expiró — inténtalo de nuevo';
else msg = `Error de geolocalización (código ${err.code})`;
} else {
msg = err && err.message ? err.message : 'No se pudo obtener tu ubicación';
}
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]);
/* ── Scanner → Search handoff ──────────────────────────── */
function handleScanSelectMedicine(medicineName) {
setSearchQuery(medicineName);
setSelectedMedicine(null);
setPharmacies([]);
setScreen('search');
}
/* ── Screens ─────────────────────────────────────────────── */
if (screen === 'home') {
return (
<HomeView
onScanClick={() => setScreen('scan')}
onSearchClick={() => setScreen('search')}
/>
);
}
if (screen === 'scan') {
return (
<ScannerView
onClose={() => setScreen('home')}
onSelectMedicine={handleScanSelectMedicine}
/>
);
}
// screen === 'search'
return (
<>
<header className="app-header">
<button
className="back-to-home-btn"
onClick={() => {
setSearchQuery('');
setSelectedMedicine(null);
setPharmacies([]);
setScreen('home');
}}
aria-label="Volver al inicio"
>
Inicio
</button>
<h1>💊 FarmaClic</h1>
<p>Encuentra tu medicamento en farmacias cercanas</p>
</header>
<main className="app-main">
<SearchBar
value={searchQuery}
onChange={setSearchQuery}
placeholder="Buscar un medicamento..."
/>
{loading && <div className="loading">Buscando...</div>}
{searchQuery && !selectedMedicine && (
<MedicineResults
medicines={medicines}
onSelect={setSelectedMedicine}
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>Principio 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>
)}
</main>
</>
);
}
export default PublicView;