feat: add sort by distance and directions to ProductView
Run Tests on Branches / Detect Changes (push) Successful in 13s
Run Tests on Branches / Backend Tests (push) Failing after 17s
Run Tests on Branches / Frontend Tests (push) Failing after 16s
Run Tests on Branches / Frontend Mobile Tests (push) Failing after 18s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / Detect Changes (push) Successful in 13s
Run Tests on Branches / Backend Tests (push) Failing after 17s
Run Tests on Branches / Frontend Tests (push) Failing after 16s
Run Tests on Branches / Frontend Mobile Tests (push) Failing after 18s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
- Added sort by distance functionality (same as SearchView) - Added PharmacyList component with distance display - Added 'Cómo llegar' button in map popup (Google Maps directions) - Uses haversineKm for distance calculation
This commit is contained in:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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)} €
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user