Feat/parapharmacy api #38

Merged
Ichitux merged 25 commits from feat/parapharmacy-api into main 2026-07-16 15:38:59 +00:00
2 changed files with 87 additions and 25 deletions
Showing only changes of commit de8fdbb904 - Show all commits
@@ -34,6 +34,15 @@ function PharmacyMap({ pharmacies }) {
<strong>{pharmacy.name}</strong><br /> <strong>{pharmacy.name}</strong><br />
{pharmacy.address} {pharmacy.address}
{pharmacy.phone && <><br />{pharmacy.phone}</>} {pharmacy.phone && <><br />{pharmacy.phone}</>}
<br />
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${pharmacy.latitude},${pharmacy.longitude}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#2563eb', marginTop: '8px', display: 'inline-block' }}
>
📍 Cómo llegar
</a>
</Popup> </Popup>
</Marker> </Marker>
))} ))}
+78 -25
View File
@@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import PharmacyMap from '../components/PharmacyMap'; import PharmacyMap from '../components/PharmacyMap';
import PharmacyList from '../components/PharmacyList';
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
import './ProductView.css'; import './ProductView.css';
export default function ProductView({ source, id, onBack }) { export default function ProductView({ source, id, onBack }) {
@@ -8,6 +10,50 @@ export default function ProductView({ source, id, onBack }) {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [pharmacies, setPharmacies] = useState([]); const [pharmacies, setPharmacies] = useState([]);
const [loadingPharmacies, setLoadingPharmacies] = useState(false); const [loadingPharmacies, setLoadingPharmacies] = useState(false);
const [sortByDistance, setSortByDistance] = useState(false);
const [userPosition, setUserPosition] = useState(null);
const [positionSource, setPositionSource] = useState(null);
const [locating, setLocating] = useState(false);
const [locationError, setLocationError] = useState('');
const hasSavedCoords = product?.latitude != null && product?.longitude != null;
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]);
const handleSortByDistance = async () => {
if (sortByDistance) {
setSortByDistance(false);
return;
}
setLocationError('');
setLocating(true);
try {
const pos = await getUserPosition();
setUserPosition(pos);
setPositionSource('browser');
setSortByDistance(true);
} 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 ubicación tardó demasiado.';
}
setLocationError(msg);
} finally {
setLocating(false);
}
};
useEffect(() => { useEffect(() => {
loadProduct(); loadProduct();
@@ -152,31 +198,38 @@ export default function ProductView({ source, id, onBack }) {
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'} Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
</h3> </h3>
<PharmacyMap pharmacies={pharmacies} /> <div className="pharmacy-controls">
<button
<div className="pharmacies-grid"> className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
{pharmacies.map((pharmacy) => ( onClick={handleSortByDistance}
<div key={pharmacy.id} className="product-pharmacy-card"> disabled={locating}
<h4 className="product-pharmacy-name">{pharmacy.name}</h4> >
<p className="product-pharmacy-address">{pharmacy.address}</p> {locating
{pharmacy.phone && ( ? '📍 Localizando…'
<p className="product-pharmacy-phone">{pharmacy.phone}</p> : sortByDistance
)} ? '📍 Ordenado por distancia · Reset'
<div className="product-pharmacy-status"> : '📍 Ordenar por distancia'}
{pharmacy.price && ( </button>
<span className="product-pharmacy-price"> {sortByDistance && positionSource && (
{parseFloat(pharmacy.price).toFixed(2)} &euro; <span className="location-source">Usando tu ubicación</span>
</span> )}
)} {locationError && (
{pharmacy.stock !== undefined && ( <span className="location-error">
<span className={`product-pharmacy-stock ${pharmacy.stock > 20 ? 'in-stock' : pharmacy.stock > 0 ? 'low-stock' : 'out-of-stock'}`}> {locationError}
{pharmacy.stock > 20 ? 'En Stock' : pharmacy.stock > 0 ? `Stock Bajo (${pharmacy.stock})` : 'Sin Stock'} <button className="retry-location-btn" onClick={handleSortByDistance}>
</span> Reintentar
)} </button>
</div> </span>
</div> )}
))}
</div> </div>
<PharmacyMap pharmacies={displayedPharmacies} />
<PharmacyList
pharmacies={displayedPharmacies}
loading={loadingPharmacies}
userPosition={sortByDistance ? userPosition : null}
/>
</> </>
) : null} ) : null}
</div> </div>