Files
FarmaFinder/apps/frontend/src/components/PharmacyList.jsx
T
Antoni Nuñez Romeu a58ce306bf
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled
Missing files from commit
2026-07-07 17:44:36 +02:00

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;