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:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Compute "open now" / "always open" for a pharmacy whose opening_hours is
|
||||
* stored as JSON in the { mon, tue, ..., sun } shape produced by
|
||||
* apps/API/opening-hours-osm.js.
|
||||
*
|
||||
* 24/7 is internally represented as every day ["00:00", "24:00"]. The
|
||||
* literal "24:00" is not valid HH:mm ISO; we accept it as 1440 minutes
|
||||
* (00:00 of the next day) so range math works, and treat a 24h week as
|
||||
* the dedicated `kind: '24h'` result.
|
||||
*
|
||||
* Midnight-crossing ranges (close <= open) are detected by comparing
|
||||
* the previous day's range when "now" is in the early hours.
|
||||
*/
|
||||
|
||||
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
|
||||
function parse(raw) {
|
||||
if (raw == null || raw === '') return null;
|
||||
if (typeof raw === 'object') return raw;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toMin(hm) {
|
||||
if (hm == null) return null;
|
||||
const parts = String(hm).split(':');
|
||||
if (parts.length !== 2) return null;
|
||||
const h = Number(parts[0]);
|
||||
const m = Number(parts[1]);
|
||||
if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
|
||||
if (h === 24 && m === 0) return 1440;
|
||||
if (h < 0 || h > 24 || m < 0 || m >= 60) return null;
|
||||
return h * 60 + m;
|
||||
}
|
||||
|
||||
function findNextOpen(h, now) {
|
||||
for (let off = 1; off <= 7; off++) {
|
||||
const key = DAYS[(now.getDay() + off) % 7];
|
||||
const r = h[key];
|
||||
if (Array.isArray(r) && r.length === 2) {
|
||||
return { day: key, time: r[0] };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isAlwaysOpen(rawHours) {
|
||||
const h = parse(rawHours);
|
||||
if (!h || typeof h !== 'object') 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 isOpenNow(rawHours, now = new Date()) {
|
||||
const h = parse(rawHours);
|
||||
if (!h) return null;
|
||||
|
||||
if (isAlwaysOpen(h)) {
|
||||
return { isOpen: true, kind: '24h' };
|
||||
}
|
||||
|
||||
const dayKey = DAYS[now.getDay()];
|
||||
const range = h[dayKey];
|
||||
|
||||
if (!Array.isArray(range) || range.length !== 2) {
|
||||
return { isOpen: false, kind: 'closed', nextOpen: findNextOpen(h, now) };
|
||||
}
|
||||
|
||||
const openM = toMin(range[0]);
|
||||
const closeM = toMin(range[1]);
|
||||
if (openM == null || closeM == null) return null;
|
||||
|
||||
const nowM = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
// First: is "now" still inside yesterday's midnight-crossing range?
|
||||
// (Today is irrelevant while we're still in the previous day's after-midnight tail.)
|
||||
const yestKey = DAYS[(now.getDay() + 6) % 7];
|
||||
const yest = h[yestKey];
|
||||
if (Array.isArray(yest) && yest.length === 2) {
|
||||
const yOpen = toMin(yest[0]);
|
||||
const yClose = toMin(yest[1]);
|
||||
if (yOpen != null && yClose != null && yClose <= yOpen && nowM < yClose) {
|
||||
return { isOpen: true, kind: 'open', closesAt: yest[1] };
|
||||
}
|
||||
}
|
||||
|
||||
// Midnight-crossing: today's range spans past 24:00.
|
||||
if (closeM <= openM) {
|
||||
if (nowM >= openM) {
|
||||
return { isOpen: true, kind: 'open', closesAt: range[1] };
|
||||
}
|
||||
return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null };
|
||||
}
|
||||
|
||||
if (nowM < openM) {
|
||||
return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null };
|
||||
}
|
||||
if (nowM >= closeM) {
|
||||
return { isOpen: false, kind: 'after-close', nextOpen: findNextOpen(h, now) };
|
||||
}
|
||||
return { isOpen: true, kind: 'open', closesAt: range[1] };
|
||||
}
|
||||
Reference in New Issue
Block a user