183 lines
6.3 KiB
React
183 lines
6.3 KiB
React
import React, { useEffect, useState } from 'react';
|
|
import './PharmacyList.css';
|
|
import { haversineKm, formatDistance } from '../utils/geo';
|
|
import { getOpenStatus } from '../utils/hours';
|
|
import {
|
|
pushSupported,
|
|
isSubscribedLocally,
|
|
subscribeToPush,
|
|
unsubscribeFromPush,
|
|
} from '../utils/notifications.js';
|
|
|
|
function PharmacyList({ pharmacies, loading, userPosition, medicine, currentUser, onLoginRequest }) {
|
|
if (loading) {
|
|
return (
|
|
<div className="loading-pharmacies">
|
|
<p>Cargando farmacias...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (pharmacies.length === 0) {
|
|
return (
|
|
<div className="no-pharmacies">
|
|
<p>No se encontraron farmacias con este medicamento</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="pharmacy-list">
|
|
<h3 className="pharmacy-list-title">
|
|
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
|
|
</h3>
|
|
<div className="pharmacy-grid">
|
|
{pharmacies.map((pharmacy) => {
|
|
const distanceKm =
|
|
userPosition && pharmacy.latitude != null && pharmacy.longitude != null
|
|
? haversineKm(userPosition.lat, userPosition.lon, pharmacy.latitude, pharmacy.longitude)
|
|
: null;
|
|
return (
|
|
<PharmacyCard
|
|
key={pharmacy.id}
|
|
pharmacy={pharmacy}
|
|
distanceKm={distanceKm}
|
|
medicine={medicine}
|
|
currentUser={currentUser}
|
|
onLoginRequest={onLoginRequest}
|
|
userPosition={userPosition}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PharmacyCard({ pharmacy, distanceKm, medicine, currentUser, onLoginRequest, userPosition }) {
|
|
const nregistro = medicine?.nregistro || medicine?.id;
|
|
const supported = pushSupported();
|
|
const outOfStock = pharmacy.stock !== undefined && pharmacy.stock <= 0;
|
|
const showBell = Boolean(medicine && nregistro && outOfStock);
|
|
|
|
const [subscribed, setSubscribed] = useState(() =>
|
|
showBell ? isSubscribedLocally(nregistro, pharmacy.id) : false
|
|
);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
if (showBell) setSubscribed(isSubscribedLocally(nregistro, pharmacy.id));
|
|
}, [nregistro, pharmacy.id, showBell]);
|
|
|
|
async function handleBell(e) {
|
|
e.stopPropagation();
|
|
if (!currentUser) {
|
|
onLoginRequest?.();
|
|
return;
|
|
}
|
|
if (!supported) {
|
|
setError('Las notificaciones requieren iOS 16.4+ y este sitio instalado como app (Compartir → Añadir a Pantalla de Inicio).');
|
|
return;
|
|
}
|
|
if (busy) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
if (subscribed) {
|
|
await unsubscribeFromPush(nregistro, pharmacy.id);
|
|
setSubscribed(false);
|
|
} else {
|
|
await subscribeToPush(nregistro, medicine.name, pharmacy.id);
|
|
setSubscribed(true);
|
|
}
|
|
} catch (err) {
|
|
console.error('[notify] pharmacy toggle failed:', err);
|
|
setError(err.message || 'No se pudo actualizar la suscripción');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const openStatus = getOpenStatus(pharmacy.opening_hours);
|
|
|
|
return (
|
|
<div className="pharmacy-card">
|
|
<div className="pharmacy-header">
|
|
<h4>🏥 {pharmacy.name}</h4>
|
|
<div className="pharmacy-header-actions">
|
|
{distanceKm != null && (
|
|
<span className="pharmacy-distance">{formatDistance(distanceKm)}</span>
|
|
)}
|
|
{showBell && (
|
|
<button
|
|
type="button"
|
|
className={`notify-bell${subscribed && currentUser ? ' notify-bell--on' : ''}${!currentUser ? ' notify-bell--locked' : ''}`}
|
|
onClick={handleBell}
|
|
disabled={busy}
|
|
aria-pressed={subscribed && !!currentUser}
|
|
aria-label={
|
|
!currentUser
|
|
? 'Inicia sesión para activar notificaciones'
|
|
: subscribed
|
|
? 'Desactivar notificaciones para esta farmacia'
|
|
: 'Notificarme cuando llegue a esta farmacia'
|
|
}
|
|
title={
|
|
!currentUser
|
|
? 'Inicia sesión para activar notificaciones'
|
|
: subscribed
|
|
? 'Notificaciones activadas para esta farmacia — clic para desactivar'
|
|
: 'Notificarme cuando llegue a esta farmacia'
|
|
}
|
|
>
|
|
{subscribed && currentUser ? '🔔' : '🔕'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="pharmacy-details">
|
|
{openStatus && (
|
|
<p className={`pharmacy-hours pharmacy-hours--${openStatus.status}`}>
|
|
<span className="pharmacy-hours-dot" /> {openStatus.label}
|
|
</p>
|
|
)}
|
|
<p className="pharmacy-address">📍 {pharmacy.address}</p>
|
|
{pharmacy.phone && (
|
|
<p className="pharmacy-phone">📞 {pharmacy.phone}</p>
|
|
)}
|
|
{error && <p className="notify-error">{error}</p>}
|
|
{pharmacy.latitude != null && pharmacy.longitude != null && (
|
|
<a
|
|
className="pharmacy-directions"
|
|
href={
|
|
userPosition
|
|
? `https://www.google.com/maps/dir/?api=1&origin=${userPosition.lat},${userPosition.lon}&destination=${pharmacy.latitude},${pharmacy.longitude}`
|
|
: `https://www.google.com/maps/dir/?api=1&destination=${pharmacy.latitude},${pharmacy.longitude}`
|
|
}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<polygon points="3 11 22 2 13 21 11 13 3 11" />
|
|
</svg>
|
|
Cómo llegar
|
|
</a>
|
|
)}
|
|
<div className="pharmacy-pricing">
|
|
{pharmacy.price && (
|
|
<span className="price">€{parseFloat(pharmacy.price).toFixed(2)}</span>
|
|
)}
|
|
{pharmacy.stock !== undefined && (
|
|
<span className={`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>
|
|
);
|
|
}
|
|
|
|
export default PharmacyList;
|