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

- 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:
Antoni Nuñez Romeu
2026-07-27 14:32:05 +02:00
parent d43cbfa44e
commit e3e7d2f60b
18 changed files with 678 additions and 73 deletions
+25 -3
View File
@@ -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