feat: pharmacy hours status and open now filter
Run Tests on Branches / Detect Changes (push) Successful in 17s
Run Tests on Branches / Backend Tests (push) Successful in 2m44s
Run Tests on Branches / Frontend Tests (push) Successful in 1m59s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Detect Changes (push) Successful in 17s
Run Tests on Branches / Backend Tests (push) Successful in 2m44s
Run Tests on Branches / Frontend Tests (push) Successful in 1m59s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
- Add is_open_now and is_always_open helpers in backend - Add backend endpoint enrichment with is_open and is_24h fields - Add client-side getOpenStatus with 24h, opens-at, opens-tomorrow support - Add open-now filter toggle in PublicView and SearchView - Add pharmacy no-hours fallback display - Add sticky pharmacy controls on scroll - Add admin hours editor with 24h toggle - Add translations (es/ca) for all hour-related strings - Add backend tests for hours logic and pharmacy endpoint - Remove unused backup test files
This commit is contained in:
@@ -28,10 +28,10 @@
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.0",
|
||||
"@testing-library/react": "^14.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,11 @@
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.pharmacy-hours--unknown {
|
||||
color: var(--on-surface-variant);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.pharmacy-pricing {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -107,7 +107,10 @@ function PharmacyCard({ pharmacy, distanceKm, medicine, currentUser, onLoginRequ
|
||||
return (
|
||||
<div className="pharmacy-card">
|
||||
<div className="pharmacy-header">
|
||||
<h4>🏥 {pharmacy.name}</h4>
|
||||
<h4>
|
||||
🏥 {pharmacy.name}
|
||||
{pharmacy.is_24h && <span className="pharmacy-badge pharmacy-badge--24h" aria-label={t('pharmacy.badge24h')}>24h</span>}
|
||||
</h4>
|
||||
<div className="pharmacy-header-actions">
|
||||
{distanceKm != null && (
|
||||
<span className="pharmacy-distance">{formatDistance(distanceKm)}</span>
|
||||
@@ -142,7 +145,7 @@ function PharmacyCard({ pharmacy, distanceKm, medicine, currentUser, onLoginRequ
|
||||
<div className="pharmacy-details">
|
||||
{openStatus && (
|
||||
<p className={`pharmacy-hours pharmacy-hours--${openStatus.status}`}>
|
||||
<span className="pharmacy-hours-dot" /> {openStatus.label}
|
||||
<span className="pharmacy-hours-dot" /> {openStatus.labelKey && openStatus.labelParams ? t(openStatus.labelKey, openStatus.labelParams) : openStatus.label}
|
||||
</p>
|
||||
)}
|
||||
<p className="pharmacy-address">📍 {pharmacy.address}</p>
|
||||
|
||||
@@ -31,7 +31,7 @@ function PharmacyMap({ pharmacies }) {
|
||||
{located.map(pharmacy => (
|
||||
<Marker key={pharmacy.id} position={[pharmacy.latitude, pharmacy.longitude]}>
|
||||
<Popup>
|
||||
<strong>{pharmacy.name}</strong><br />
|
||||
<strong>{pharmacy.name} {pharmacy.is_24h && <span className="map-badge-24h">24h</span>}</strong><br />
|
||||
{pharmacy.address}
|
||||
{pharmacy.phone && <><br />{pharmacy.phone}</>}
|
||||
<br />
|
||||
|
||||
@@ -1,49 +1,8 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import './AdminComponents.css';
|
||||
import { DAY_KEYS, DAY_LABEL } from '../../utils/hours';
|
||||
import { DAY_KEYS, DAY_LABEL, emptyHoursDraft, hoursToDraft, draftToHours, makeAlwaysOpenDraft, isAlwaysOpen } from '../../utils/hours';
|
||||
import { useTranslation } from '../../i18n';
|
||||
|
||||
function emptyHoursDraft() {
|
||||
const draft = {};
|
||||
for (const day of DAY_KEYS) {
|
||||
draft[day] = { open: '09:00', close: '21:00', closed: true };
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
function hoursToDraft(raw) {
|
||||
let parsed = null;
|
||||
if (raw) {
|
||||
try { parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; }
|
||||
catch { parsed = null; }
|
||||
}
|
||||
const draft = {};
|
||||
for (const day of DAY_KEYS) {
|
||||
const v = parsed && parsed[day];
|
||||
if (Array.isArray(v) && v.length === 2) {
|
||||
draft[day] = { open: v[0], close: v[1], closed: false };
|
||||
} else {
|
||||
draft[day] = { open: '09:00', close: '21:00', closed: true };
|
||||
}
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
function draftToHours(draft) {
|
||||
const out = {};
|
||||
let hasAny = false;
|
||||
for (const day of DAY_KEYS) {
|
||||
const d = draft[day];
|
||||
if (d && !d.closed && d.open && d.close) {
|
||||
out[day] = [d.open, d.close];
|
||||
hasAny = true;
|
||||
} else {
|
||||
out[day] = null;
|
||||
}
|
||||
}
|
||||
return hasAny ? out : null;
|
||||
}
|
||||
|
||||
/** Distance in metres between two WGS84 points */
|
||||
function haversineMeters(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371000;
|
||||
@@ -629,6 +588,22 @@ function PharmacyManagement() {
|
||||
<fieldset className="hours-editor">
|
||||
<legend>{t('admin.pharmacy.openingHours')}</legend>
|
||||
<p className="hours-editor-hint">{t('admin.pharmacy.dayClosed')}</p>
|
||||
<label className="hours-24h-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAlwaysOpen(draftToHours(hoursDraft))}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setHoursDraft(makeAlwaysOpenDraft());
|
||||
} else {
|
||||
if (window.confirm(t('admin.pharmacy.confirmDisable24h'))) {
|
||||
setHoursDraft(emptyHoursDraft());
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{t('admin.pharmacy.alwaysOpen')}
|
||||
</label>
|
||||
{DAY_KEYS.map((day) => {
|
||||
const d = hoursDraft[day];
|
||||
return (
|
||||
|
||||
@@ -66,6 +66,17 @@ const ca = {
|
||||
'pharmacy.notifyWhenArrives': 'Notificar-me quan arribi a aquesta farmàcia',
|
||||
'pharmacy.notificationsActivatedPharmacy': 'Notificacions activades per a aquesta farmàcia — clic per desactivar',
|
||||
'pharmacy.notificationsRequired': 'Les notificacions requereixen iOS 16.4+ i aquest lloc instal·lat com a app (Compartir → Afegir a Pantalla d\'Inici).',
|
||||
'pharmacy.openNow': 'Obert · Tanca a les {{time}}',
|
||||
'pharmacy.closedAllDay': 'Tancat',
|
||||
'pharmacy.opensAt': 'Tancat · Obre a les {{time}}',
|
||||
'pharmacy.opensTomorrow': 'Tancat · Obre demà a les {{time}}',
|
||||
'pharmacy.opensDay': 'Tancat · Obre el {{day}} a les {{time}}',
|
||||
'pharmacy.alwaysOpen': 'Obert 24h',
|
||||
'pharmacy.filterOpenNow': 'Mostrar només obertes ara',
|
||||
'pharmacy.filterOpenNowActive': 'Només obertes ara',
|
||||
'pharmacy.badge24h': '24h',
|
||||
'pharmacy.noHours': 'Sense horari disponible',
|
||||
'pharmacy.filterNoResults': 'Cap farmàcia oberta ara. Desactiva el filtre per veure-les totes.',
|
||||
|
||||
// ProductResults
|
||||
'product.sinReceta': 'Sense Recepta',
|
||||
@@ -338,6 +349,8 @@ const ca = {
|
||||
'admin.pharmacy.apiNotFound': 'L\'app no ha pogut connectar amb l\'API (404). Useu http://localhost:3000 amb frontend i backend actius.',
|
||||
'admin.pharmacy.geocodificationNotFound': 'Servei de geocodificació no trobat. Actualitzeu el backend i reinicieu-lo.',
|
||||
'admin.pharmacy.searchFailed': 'Cerca fallida (HTTP',
|
||||
'admin.pharmacy.alwaysOpen': '24 hores (oberta tot el dia)',
|
||||
'admin.pharmacy.confirmDisable24h': 'Desactivar 24h? Es descartaran els horaris actuals.',
|
||||
'admin.pharmacy.dayClosed': 'Marqueu un dia com a Tancat si la farmàcia no obre aquest dia.',
|
||||
'admin.pharmacy.saveError': 'Error en desar farmàcia',
|
||||
'admin.pharmacy.radius': 'Radi (m)',
|
||||
|
||||
@@ -66,6 +66,17 @@ const es = {
|
||||
'pharmacy.notifyWhenArrives': 'Notificarme cuando llegue a esta farmacia',
|
||||
'pharmacy.notificationsActivatedPharmacy': 'Notificaciones activadas para esta farmacia — clic para desactivar',
|
||||
'pharmacy.notificationsRequired': 'Las notificaciones requieren iOS 16.4+ y este sitio instalado como app (Compartir → Añadir a Pantalla de Inicio).',
|
||||
'pharmacy.openNow': 'Abierto · Cierra a las {{time}}',
|
||||
'pharmacy.closedAllDay': 'Cerrado',
|
||||
'pharmacy.opensAt': 'Cerrado · Abre a las {{time}}',
|
||||
'pharmacy.opensTomorrow': 'Cerrado · Abre mañana a las {{time}}',
|
||||
'pharmacy.opensDay': 'Cerrado · Abre el {{day}} a las {{time}}',
|
||||
'pharmacy.alwaysOpen': 'Abierto 24h',
|
||||
'pharmacy.filterOpenNow': 'Mostrar solo abiertas ahora',
|
||||
'pharmacy.filterOpenNowActive': 'Solo abiertas ahora',
|
||||
'pharmacy.badge24h': '24h',
|
||||
'pharmacy.noHours': 'Sin horario disponible',
|
||||
'pharmacy.filterNoResults': 'Ninguna farmacia abierta ahora. Desactiva el filtro para ver todas.',
|
||||
|
||||
// ProductResults
|
||||
'product.sinReceta': 'Sin Receta',
|
||||
@@ -340,6 +351,8 @@ const es = {
|
||||
'admin.pharmacy.apiNotFound': 'La app no pudo conectar con la API (404). Usa http://localhost:3000 con frontend y backend activos.',
|
||||
'admin.pharmacy.geocodingNotFound': 'Servicio de geocodificación no encontrado. Actualiza el backend y reinícialo.',
|
||||
'admin.pharmacy.searchFailed': 'Búsqueda fallida (HTTP',
|
||||
'admin.pharmacy.alwaysOpen': '24 horas (abierta todo el día)',
|
||||
'admin.pharmacy.confirmDisable24h': '¿Desactivar 24h? Se descartarán los horarios actuales.',
|
||||
'admin.pharmacy.dayClosed': 'Marca un día como Cerrado si la farmacia no abre ese día.',
|
||||
'admin.pharmacy.saveError': 'Error al guardar farmacia',
|
||||
'admin.pharmacy.radius': 'Radio (m)',
|
||||
|
||||
@@ -27,14 +27,12 @@ function parseHours(raw) {
|
||||
}
|
||||
}
|
||||
|
||||
function findNextOpen(hours, now) {
|
||||
function findNextOpenInfo(hours, now) {
|
||||
for (let offset = 1; offset <= 7; offset++) {
|
||||
const day = DAYS[(now.getDay() + offset) % 7];
|
||||
const range = hours[day];
|
||||
if (Array.isArray(range) && range.length === 2) {
|
||||
const openStr = range[0];
|
||||
if (offset === 1) return `mañana a las ${openStr}`;
|
||||
return `${DAY_LABELS[day]} a las ${openStr}`;
|
||||
return { day, time: range[0], offset };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -42,14 +40,24 @@ function findNextOpen(hours, now) {
|
||||
|
||||
export function getOpenStatus(rawHours, now = new Date()) {
|
||||
const hours = parseHours(rawHours);
|
||||
if (!hours) return null;
|
||||
if (!hours) return { status: 'unknown', label: 'Sin horario', labelKey: 'pharmacy.noHours', labelParams: {} };
|
||||
|
||||
if (isAlwaysOpen(hours)) {
|
||||
return { status: 'open', label: 'Abierto 24h', labelKey: 'pharmacy.alwaysOpen', labelParams: {} };
|
||||
}
|
||||
|
||||
const day = DAYS[now.getDay()];
|
||||
const range = hours[day];
|
||||
|
||||
if (!Array.isArray(range) || range.length !== 2) {
|
||||
const next = findNextOpen(hours, now);
|
||||
return { status: 'closed', label: next ? `Cerrado · Abre ${next}` : 'Cerrado' };
|
||||
const next = findNextOpenInfo(hours, now);
|
||||
if (!next) {
|
||||
return { status: 'closed', label: 'Cerrado', labelKey: 'pharmacy.closedAllDay', labelParams: {} };
|
||||
}
|
||||
if (next.offset === 1) {
|
||||
return { status: 'closed', label: `Cerrado · Abre mañana a las ${next.time}`, labelKey: 'pharmacy.opensTomorrow', labelParams: { time: next.time } };
|
||||
}
|
||||
return { status: 'closed', label: `Cerrado · Abre el ${DAY_LABELS[next.day]} a las ${next.time}`, labelKey: 'pharmacy.opensDay', labelParams: { day: DAY_LABELS[next.day], time: next.time } };
|
||||
}
|
||||
|
||||
const openMins = parseHM(range[0]);
|
||||
@@ -59,15 +67,81 @@ export function getOpenStatus(rawHours, now = new Date()) {
|
||||
const nowMins = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
if (nowMins < openMins) {
|
||||
return { status: 'closed', label: `Cerrado · Abre a las ${range[0]}` };
|
||||
return { status: 'closed', label: `Cerrado · Abre a las ${range[0]}`, labelKey: 'pharmacy.opensAt', labelParams: { time: range[0] } };
|
||||
}
|
||||
if (nowMins >= closeMins) {
|
||||
const next = findNextOpen(hours, now);
|
||||
return { status: 'closed', label: next ? `Cerrado · Abre ${next}` : 'Cerrado' };
|
||||
const next = findNextOpenInfo(hours, now);
|
||||
if (!next) {
|
||||
return { status: 'closed', label: 'Cerrado', labelKey: 'pharmacy.closedAllDay', labelParams: {} };
|
||||
}
|
||||
if (next.offset === 1) {
|
||||
return { status: 'closed', label: `Cerrado · Abre mañana a las ${next.time}`, labelKey: 'pharmacy.opensTomorrow', labelParams: { time: next.time } };
|
||||
}
|
||||
return { status: 'closed', label: `Cerrado · Abre el ${DAY_LABELS[next.day]} a las ${next.time}`, labelKey: 'pharmacy.opensDay', labelParams: { day: DAY_LABELS[next.day], time: next.time } };
|
||||
}
|
||||
return { status: 'open', label: `Abierto · Cierra a las ${range[1]}` };
|
||||
return { status: 'open', label: `Abierto · Cierra a las ${range[1]}`, labelKey: 'pharmacy.openNow', labelParams: { time: range[1] } };
|
||||
}
|
||||
|
||||
export function emptyHours() {
|
||||
return { sun: null, mon: null, tue: null, wed: null, thu: null, fri: null, sat: null };
|
||||
}
|
||||
|
||||
export function emptyHoursDraft() {
|
||||
const draft = {};
|
||||
for (const day of DAYS) {
|
||||
draft[day] = { open: '09:00', close: '21:00', closed: true };
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function hoursToDraft(raw) {
|
||||
let parsed = null;
|
||||
if (raw) {
|
||||
try { parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; }
|
||||
catch { parsed = null; }
|
||||
}
|
||||
const draft = {};
|
||||
for (const day of DAYS) {
|
||||
const v = parsed && parsed[day];
|
||||
if (Array.isArray(v) && v.length === 2) {
|
||||
draft[day] = { open: v[0], close: v[1], closed: false };
|
||||
} else {
|
||||
draft[day] = { open: '09:00', close: '21:00', closed: true };
|
||||
}
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function draftToHours(draft) {
|
||||
const out = {};
|
||||
let hasAny = false;
|
||||
for (const day of DAYS) {
|
||||
const d = draft[day];
|
||||
if (d && !d.closed && d.open && d.close) {
|
||||
out[day] = [d.open, d.close];
|
||||
hasAny = true;
|
||||
} else {
|
||||
out[day] = null;
|
||||
}
|
||||
}
|
||||
return hasAny ? out : null;
|
||||
}
|
||||
|
||||
export function isAlwaysOpen(rawHours) {
|
||||
const h = parseHours(rawHours);
|
||||
if (!h) return false;
|
||||
for (const d of DAYS) {
|
||||
const r = h[d];
|
||||
if (!Array.isArray(r) || r.length !== 2) return false;
|
||||
if (r[0] !== '00:00' || r[1] !== '24:00') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function makeAlwaysOpenDraft() {
|
||||
const draft = {};
|
||||
for (const day of DAYS) {
|
||||
draft[day] = { open: '00:00', close: '24:00', closed: false };
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ function PublicView({
|
||||
const [userPosition, setUserPosition] = useState(null);
|
||||
const [positionSource, setPositionSource] = useState(null); // 'profile' | 'browser'
|
||||
const [sortByDistance, setSortByDistance] = useState(false);
|
||||
const [openNow, setOpenNow] = useState(false);
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [locationError, setLocationError] = useState('');
|
||||
|
||||
@@ -177,8 +178,12 @@ function PublicView({
|
||||
};
|
||||
|
||||
const displayedPharmacies = useMemo(() => {
|
||||
if (!sortByDistance || !userPosition) return pharmacies;
|
||||
return [...pharmacies].sort((a, b) => {
|
||||
let filtered = pharmacies;
|
||||
if (openNow) {
|
||||
filtered = pharmacies.filter(p => p.is_open === true || p.opening_hours == null);
|
||||
}
|
||||
if (!sortByDistance || !userPosition) return filtered;
|
||||
return [...filtered].sort((a, b) => {
|
||||
if (a.latitude == null || a.longitude == null) return 1;
|
||||
if (b.latitude == null || b.longitude == null) return -1;
|
||||
return (
|
||||
@@ -186,7 +191,7 @@ function PublicView({
|
||||
haversineKm(userPosition.lat, userPosition.lon, b.latitude, b.longitude)
|
||||
);
|
||||
});
|
||||
}, [pharmacies, sortByDistance, userPosition]);
|
||||
}, [pharmacies, openNow, sortByDistance, userPosition]);
|
||||
|
||||
/* ── Scanner → Search handoff ──────────────────────────── */
|
||||
function handleScanSelectMedicine(medicineName) {
|
||||
@@ -323,6 +328,13 @@ function PublicView({
|
||||
|
||||
{pharmacies.length > 0 && (
|
||||
<div className="pharmacy-controls">
|
||||
<button
|
||||
className={`open-now-toggle ${openNow ? 'active' : ''}`}
|
||||
onClick={() => setOpenNow(o => !o)}
|
||||
aria-pressed={openNow}
|
||||
>
|
||||
{openNow ? '🟢 Solo abiertas ahora' : '⏱ Mostrar solo abiertas ahora'}
|
||||
</button>
|
||||
<button
|
||||
className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
|
||||
onClick={handleSortByDistance}
|
||||
@@ -345,6 +357,11 @@ function PublicView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openNow && displayedPharmacies.length === 0 && (
|
||||
<div className="open-now-empty">
|
||||
<p>Ninguna farmacia abierta ahora — desactiva el filtro para ver todas.</p>
|
||||
</div>
|
||||
)}
|
||||
<PharmacyMap pharmacies={displayedPharmacies} />
|
||||
<PharmacyList
|
||||
pharmacies={displayedPharmacies}
|
||||
|
||||
@@ -296,6 +296,11 @@
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: var(--surface);
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.sort-distance-button {
|
||||
@@ -312,7 +317,7 @@
|
||||
|
||||
.sort-distance-button:hover:not(:disabled) {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
color: #151c17;
|
||||
}
|
||||
|
||||
.sort-distance-button.active {
|
||||
|
||||
@@ -6,6 +6,7 @@ import PharmacyList from '../components/PharmacyList';
|
||||
import PharmacyMap from '../components/PharmacyMap';
|
||||
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { getOpenStatus } from '../utils/hours';
|
||||
import './SearchView.css';
|
||||
|
||||
const suggestions = [
|
||||
@@ -26,6 +27,7 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigate
|
||||
const [userPosition, setUserPosition] = useState(null);
|
||||
const [positionSource, setPositionSource] = useState(null);
|
||||
const [sortByDistance, setSortByDistance] = useState(false);
|
||||
const [openNow, setOpenNow] = useState(false);
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [locationError, setLocationError] = useState('');
|
||||
const [recentSearches, setRecentSearches] = useState([]);
|
||||
@@ -208,8 +210,19 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigate
|
||||
};
|
||||
|
||||
const displayedPharmacies = useMemo(() => {
|
||||
if (!sortByDistance || !userPosition) return pharmacies;
|
||||
return [...pharmacies].sort((a, b) => {
|
||||
let result = pharmacies;
|
||||
if (openNow) {
|
||||
result = result.filter((p) => {
|
||||
if (p.is_open === true) return true;
|
||||
if (p.is_open == null) {
|
||||
const s = getOpenStatus(p.opening_hours);
|
||||
return s && s.status === 'open';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
if (!sortByDistance || !userPosition) return result;
|
||||
return [...result].sort((a, b) => {
|
||||
if (a.latitude == null || a.longitude == null) return 1;
|
||||
if (b.latitude == null || b.longitude == null) return -1;
|
||||
return (
|
||||
@@ -217,7 +230,7 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigate
|
||||
haversineKm(userPosition.lat, userPosition.lon, b.latitude, b.longitude)
|
||||
);
|
||||
});
|
||||
}, [pharmacies, sortByDistance, userPosition]);
|
||||
}, [pharmacies, sortByDistance, userPosition, openNow]);
|
||||
|
||||
return (
|
||||
<div className="search-view">
|
||||
@@ -359,6 +372,12 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigate
|
||||
|
||||
{pharmacies.length > 0 && (
|
||||
<div className="pharmacy-controls">
|
||||
<button
|
||||
className={`sort-distance-button ${openNow ? 'active' : ''}`}
|
||||
onClick={() => setOpenNow((v) => !v)}
|
||||
>
|
||||
{openNow ? t('pharmacy.filterOpenNowActive') : t('pharmacy.filterOpenNow')}
|
||||
</button>
|
||||
<button
|
||||
className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
|
||||
onClick={handleSortByDistance}
|
||||
@@ -388,6 +407,9 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigate
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{openNow && pharmacies.length > 0 && displayedPharmacies.length === 0 && (
|
||||
<div className="no-pharmacies">{t('pharmacy.filterNoResults')}</div>
|
||||
)}
|
||||
|
||||
<PharmacyMap pharmacies={displayedPharmacies} />
|
||||
<PharmacyList
|
||||
|
||||
Reference in New Issue
Block a user