Files
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

95 lines
6.9 KiB
Markdown

# 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