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 />
{pharmacy.address}
{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>
</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 PharmacyList from '../components/PharmacyList';
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
import './ProductView.css';
export default function ProductView({ source, id, onBack }) {
@@ -8,6 +10,50 @@ export default function ProductView({ source, id, onBack }) {
const [error, setError] = useState(null);
const [pharmacies, setPharmacies] = useState([]);
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(() => {
loadProduct();
@@ -152,31 +198,38 @@ export default function ProductView({ source, id, onBack }) {
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
</h3>
<PharmacyMap pharmacies={pharmacies} />
<div className="pharmacies-grid">
{pharmacies.map((pharmacy) => (
<div key={pharmacy.id} className="product-pharmacy-card">
<h4 className="product-pharmacy-name">{pharmacy.name}</h4>
<p className="product-pharmacy-address">{pharmacy.address}</p>
{pharmacy.phone && (
<p className="product-pharmacy-phone">{pharmacy.phone}</p>
)}
<div className="product-pharmacy-status">
{pharmacy.price && (
<span className="product-pharmacy-price">
{parseFloat(pharmacy.price).toFixed(2)} &euro;
</span>
)}
{pharmacy.stock !== undefined && (
<span className={`product-pharmacy-stock ${pharmacy.stock > 20 ? 'in-stock' : pharmacy.stock > 0 ? 'low-stock' : 'out-of-stock'}`}>
{pharmacy.stock > 20 ? 'En Stock' : pharmacy.stock > 0 ? `Stock Bajo (${pharmacy.stock})` : 'Sin Stock'}
</span>
)}
</div>
</div>
))}
<div className="pharmacy-controls">
<button
className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
onClick={handleSortByDistance}
disabled={locating}
>
{locating
? '📍 Localizando…'
: sortByDistance
? '📍 Ordenado por distancia · Reset'
: '📍 Ordenar por distancia'}
</button>
{sortByDistance && positionSource && (
<span className="location-source">Usando tu ubicación</span>
)}
{locationError && (
<span className="location-error">
{locationError}
<button className="retry-location-btn" onClick={handleSortByDistance}>
Reintentar
</button>
</span>
)}
</div>
<PharmacyMap pharmacies={displayedPharmacies} />
<PharmacyList
pharmacies={displayedPharmacies}
loading={loadingPharmacies}
userPosition={sortByDistance ? userPosition : null}
/>
</>
) : null}
</div>