Files
FarmaFinder/apps/backend/src/hours.js
T
Antoni Nuñez Romeu e3e7d2f60b
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
feat: pharmacy hours status and open now filter
- 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
2026-07-27 14:32:05 +02:00

110 lines
3.3 KiB
JavaScript

/**
* 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] };
}