Files
FarmaFinder/docs/horarios.md
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

6.9 KiB

Horarios de farmacias — Implementación

Resumen

Sistema completo para gestionar, calcular y filtrar horarios de apertura de farmacias. Implementado siguiendo el plan en docs/PLAN-HORARIOS.md.

Lo implementado

Backend (apps/backend/)

Archivo Qué hace
src/hours.js Helper puro: isOpenNow(rawHours, now?) y isAlwaysOpen(rawHours). Soporta 24/7, rangos normales, cruce de medianoche, días cerrados, 24:00 como 1440 min.
server.js Los 3 endpoints públicos (/api/medicines/:id/pharmacies, /api/products/:source/:id/pharmacies, /api/pharmacies) devuelven is_open (boolean o null) e is_24h (boolean) precalculados con la hora del servidor. Helper enrichPharmacy(row).

Frontend (apps/frontend/)

Archivo Qué hace
src/utils/hours.js getOpenStatus() refactorizado: devuelve labelKey/labelParams para i18n + label legacy. Nuevos: emptyHoursDraft(), hoursToDraft(), draftToHours() (movidos desde admin), isAlwaysOpen(), makeAlwaysOpenDraft().
src/views/PublicView.jsx Nuevo estado openNow. Botón "Mostrar solo abiertas ahora" en .pharmacy-controls. Filtro en displayedPharmacies (incluye farmacias sin horarios). Mensaje si 0 resultados.
src/components/PharmacyList.jsx Badge 24h junto al nombre. Texto de estado usa t() con claves i18n.
src/components/PharmacyMap.jsx Badge 24h en popup del marcador.
src/components/admin/PharmacyManagement.jsx Toggle "24 horas" en el editor de horarios. Al activarlo, rellena todos los días con 00:00-24:00. Al desactivarlo, confirm() antes de descartar.
src/i18n/locales/es.js Nuevas claves: pharmacy.openNow, pharmacy.closedAllDay, pharmacy.opensAt, pharmacy.opensTomorrow, pharmacy.opensDay, pharmacy.alwaysOpen, pharmacy.filterOpenNow, pharmacy.filterOpenNowActive, pharmacy.badge24h, pharmacy.filterNoResults, admin.pharmacy.alwaysOpen, admin.pharmacy.confirmDisable24h.
src/i18n/locales/ca.js Traducciones catalanas de todas las claves nuevas.

Tests

Archivo Tests
apps/backend/__tests__/hours.test.js 22 tests — isAlwaysOpen (6), isOpenNow (16): 24/7, normal, cerrado, cruce medianoche, null, malformed JSON.
apps/backend/__tests__/pharmacy-hours-endpoint.test.js 4 tests — verifica is_open/is_24h en respuesta JSON para 24h, normal, null, múltiples.
apps/frontend/src/App.test.jsx 6 tests existentes — sin regresión.
apps/frontend/src/utils/notifications.test.js 1 test existente — sin regresión.

Cómo funciona

  1. Almacenamiento: pharmacies.opening_hours como TEXT JSON. Shape: { mon: ["09:00","21:00"], tue: null, ... }. 24/7 → todos los días ["00:00","24:00"].
  2. Cálculo en servidor: Cada request a endpoints públicos ejecuta isOpenNow() con la hora del servidor. El frontend recibe is_open e is_24h ya calculados.
  3. Cálculo en cliente: getOpenStatus() existe como fallback si is_open no está presente (datos legacy).
  4. Filtro: Cliente-side. Farmacias sin horarios (opening_hours = null) no se filtran.
  5. Admin: El toggle 24h rellena los 7 días. El editor manual permite día por día.

Puntos de mejora futuros

Pendientes del plan original

  • Refresco automático cada 60s: Si la página permanece abierta mucho tiempo, el estado "abierto/cerrado" puede quedar desactualizado. Añadir setInterval de 60s en PublicView.jsx para recalcular o re-fetch.
  • Cache server-side: Si el dataset de farmacias crece (>1000), cachear isOpenNow con TTL de 1 minuto por minuto actual. Comentario TODO(cache) ya está en server.js.

UI/UX

  • Tooltip explicativo: Al pasar el ratón sobre el badge "24h", mostrar "Abierta las 24 horas del día".
  • Color en el filtro: El botón "Abiertas ahora" ganaría con un icono verde intermitente o un cambio de color más evidente.
  • Separar horas de apertura/cierre: Actualmente el editor de admin usa <input type="time"> que no acepta 24:00 como valor. Para crear una farmacia 24h hay que usar el toggle. El input manual no permite escribir 24:00.
  • Ordenación combinada: "Abiertas ahora" + "Ordenar por distancia" deberían priorizar las abiertas pero ordenadas por distancia. Actualmente primero filtra, luego ordena.

Técnicos

  • Zona horaria explícita: El servidor usa su hora local. Devolver server_now y server_tz en la respuesta para que la UI pueda mostrar "según hora del servidor". Ver pregunta abierta #1 en el plan.
  • Tests de integración real: Los tests de backend usan SQLite en memoria. Con PostgreSQL real los endpoints deben comportarse igual.
  • Parser OSM 24/7 → 24:00: El parser de OSM ya produce ["00:00","24:00"] para 24/7. Si en el futuro OSM cambia el formato, actualizar solo opening-hours-osm.js. No tocar nada más (los helpers son agnósticos al formato de entrada).
  • Cobertura frontend: Los helpers puros de hours.js no tienen tests unitarios. Añadir tests para isAlwaysOpen() frontend, emptyHoursDraft(), hoursToDraft(), draftToHours(), makeAlwaysOpenDraft().

Admin

  • Vista previa de horarios: En la lista de farmacias del admin, mostrar un resumen "L-V 9:00-21:00, S 9:00-14:00" o "24h" en vez del JSON crudo.
  • Importación masiva con horarios: El importador OSM ya trae opening_hours. El importador de datos abiertos también si el JSON incluye el campo. Verificar que los tres caminos de ingesta sigan parseando correctamente tras los cambios.

Mobile (apps/frontend-mobile/)

  • Los mismos cambios en la app móvil: El frontend móvil tiene su propia copia de i18n (apps/frontend-mobile/src/i18n/locales/es.js y ca.js) con menos claves. No se ha tocado. Habría que añadir las mismas claves pharmacy.* y replicar la lógica de filtrado/badge.

Archivos creados/modificados

Creados:

  • apps/backend/__tests__/pharmacy-hours-endpoint.test.js
  • docs/horarios.md (este)

Modificados:

  • apps/backend/server.js — import src/hours.js, helper enrichPharmacy, 3 endpoints enriquecidos
  • apps/frontend/src/utils/hours.js — getOpenStatus() con i18n, +5 nuevas exportaciones
  • apps/frontend/src/views/PublicView.jsx — filtro openNow
  • apps/frontend/src/components/PharmacyList.jsx — badge 24h, i18n en estado
  • apps/frontend/src/components/PharmacyMap.jsx — badge 24h en popup
  • apps/frontend/src/components/admin/PharmacyManagement.jsx — toggle 24h, import desde hours.js
  • apps/frontend/src/i18n/locales/es.js — 12 nuevas claves
  • apps/frontend/src/i18n/locales/ca.js — 12 nuevas claves

No tocados (intencionalmente):

  • apps/API/opening-hours-osm.js — parser OSM en producción
  • apps/backend/farmacias-webhook-import.js — ya parsea correctamente
  • apps/frontend-mobile/ — requiere移植 manual