Patentes internacionales + Forgot Pwd
Run Tests on Branches / Detect Changes (push) Successful in 19s
Run Tests on Branches / Backend Tests (push) Successful in 2m46s
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 2m24s
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 / Detect Changes (push) Successful in 19s
Run Tests on Branches / Backend Tests (push) Successful in 2m46s
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 2m24s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
# Plan — Horarios de farmacias, filtro "Abiertas Ahora" y farmacias 24h
|
||||
|
||||
> **TL;DR**
|
||||
> - **El parser OSM ya existe** (`apps/API/opening-hours-osm.js`) y cubre `24/7`, rangos, splits, wrap-around y días cerrados. Su output (`["00:00", "24:00"]` para 24/7) ya entra en BD; lo que falta es **interpretar bien `24:00`**, centralizar el cálculo "abierto/24h" en el backend, exponer un endpoint "isOpenNow" y añadir el filtro en la UI.
|
||||
> - Tres pasos incrementales, cada uno verificable de forma aislada. Ninguno rompe lo que ya hay: el campo `opening_hours` se conserva como JSON string y el admin ya lo edita.
|
||||
> - **TDD obligatorio** (red-green-refactor, ≥80% cobertura en lo que se añada) por la regla global de testing.
|
||||
|
||||
---
|
||||
|
||||
## Estado actual (lo que ya tenemos)
|
||||
|
||||
| Pieza | Ubicación | Estado |
|
||||
|-------|-----------|--------|
|
||||
| Parser OSM → JSON `{day:[open,close] \| null}` | `apps/API/opening-hours-osm.js` | Hecho + tests en `apps/backend/__tests__/opening-hours-osm.test.js` |
|
||||
| Almacenamiento `pharmacies.opening_hours TEXT` | `apps/backend/server.js` (`initDatabase`, `serializeOpeningHours`) | Hecho (PG + SQLite con migración) |
|
||||
| Ingestión desde OSM, n8n webhook y URL openData | `apps/API/*`, `apps/backend/farmacias-webhook-import.js` | Hecho — los tres caminos ya parsean horarios |
|
||||
| `getOpenStatus()` frontend | `apps/frontend/src/utils/hours.js` | Parcial: NO trata `24:00` como 24h (lo trata como "medianoche", farmacia cerrada); usa idioma hardcoded (es) en labels |
|
||||
| Editor de horarios en admin | `apps/frontend/src/components/admin/PharmacyManagement.jsx` | Hecho (filas Lun–Dom, open/close, toggle "Cerrado") |
|
||||
| Badge "Abierto / Cerrado" en lista | `apps/frontend/src/components/PharmacyList.jsx` | Hecho vía `getOpenStatus()` |
|
||||
| Filtro "Abiertas Ahora" | — | **No existe** |
|
||||
| Etiqueta "24h" en lista/mapa | — | **No existe** |
|
||||
| Endpoint que devuelva estado calculado en servidor | — | **No existe** (todo se calcula en cliente) |
|
||||
|
||||
**Diagnóstico clave:**
|
||||
1. `parseOsmOpeningHours('24/7')` produce `["00:00", "24:00"]` para todos los días. `getOpenStatus()` en frontend llama `parseHM("24:00")` → `24*60+0 = 1440`. Como `nowMins < 1440` casi siempre es `true`, nunca se evalúa el caso 24h. Necesita un caso explícito.
|
||||
2. `getOpenStatus()` calcula "ahora" en el navegador. Si el usuario cambia de zona horaria o el servidor de test no está en hora local, el resultado puede no coincidir con el esperado.
|
||||
3. `opening_hours` se devuelve crudo en `/api/medicines/:nregistro/pharmacies` y `/api/pharmacies`. El frontend debe parsearlo cada vez (ya lo hace, pero sin tests frontend).
|
||||
|
||||
---
|
||||
|
||||
## Enfoque general
|
||||
|
||||
1. **Mover el cálculo de "está abierta / es 24h" al backend**, exponerlo en un único helper puro y testable. Devolver ese dato precalculado en los endpoints públicos para que el frontend no tenga que duplicar la lógica ni parchear el caso `24:00`.
|
||||
2. **Filtrar en cliente** (no en servidor): el dataset de farmacias por búsqueda es pequeño (decenas, no miles), y el filtro debe combinarse con "ordenar por distancia". Mantener la lógica en un solo reducer/hook.
|
||||
3. **24h como estado derivado** del JSON, no como columna nueva: si todos los días son `["00:00", "24:00"]`, la farmacia es 24h. Evita migración, evita sincronización.
|
||||
4. **i18n primero**: mover los strings hardcoded de `hours.js` y `PharmacyList.jsx` a los locales (`es.js`, `ca.js`).
|
||||
|
||||
---
|
||||
|
||||
## Paso 1 — Helper de estado abierto/24h en el backend (TDD)
|
||||
|
||||
> **Objetivo:** un solo módulo `apps/backend/src/hours.js` con `isOpenNow(rawHours, now)` y `isAlwaysOpen(rawHours)` que sea la fuente de verdad. Tests Jest con casos explícitos para `24:00`, medianoche cruzando, splits (no usados en almacenamiento, pero defensivo), días cerrados.
|
||||
|
||||
### 1.1 Tests primero (RED)
|
||||
|
||||
**`apps/backend/__tests__/hours.test.js`** (nuevo)
|
||||
|
||||
```
|
||||
describe('isOpenNow')
|
||||
✓ returns null when raw is null/empty
|
||||
✓ returns null when JSON is malformed
|
||||
✓ open window covers now → { isOpen: true, kind: 'open', closesAt: '21:00' }
|
||||
✓ before opening today → { isOpen: false, kind: 'before-open', opensAt: '09:00' }
|
||||
✓ after closing today but opens tomorrow → { isOpen: false, kind: 'after-close', nextOpen: { day: 'mon', time: '09:00' } }
|
||||
✓ closed all week → { isOpen: false, kind: 'closed', nextOpen: null }
|
||||
✓ 24/7 (["00:00","24:00"]) → { isOpen: true, kind: '24h' }
|
||||
✓ 24:00 close at 23:59:59 → open; at 24:00:00 → close, nextOpen: tomorrow
|
||||
✓ midnight-crossing range: mon ["22:00","02:00"] — at 01:00 monday → open (special-case: subtract 1 day from now, check sunday range)
|
||||
— implement: if closeMins <= openMins, treat the range as [open, close+24h]; check nowMins against that
|
||||
✓ all days closed → kind 'closed'
|
||||
|
||||
describe('isAlwaysOpen')
|
||||
✓ all 7 days ["00:00","24:00"] → true
|
||||
✓ one day different → false
|
||||
✓ one day null → false
|
||||
✓ null input → false
|
||||
```
|
||||
|
||||
**Aceptación:** `npm test --workspace=farma-clic-backend` corre los nuevos tests y todos pasan **antes** de tocar otros archivos.
|
||||
|
||||
### 1.2 Implementación (GREEN)
|
||||
|
||||
**`apps/backend/src/hours.js`** (nuevo)
|
||||
|
||||
```js
|
||||
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
|
||||
function parse(raw) {
|
||||
if (!raw) return null;
|
||||
if (typeof raw === 'object') return raw;
|
||||
try { return JSON.parse(raw); } catch { return null; }
|
||||
}
|
||||
|
||||
function toMin(hm) {
|
||||
// accept "24:00" as 1440 only when minutes == 0
|
||||
const [h, m] = String(hm).split(':').map(Number);
|
||||
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;
|
||||
}
|
||||
|
||||
export function isAlwaysOpen(rawHours) {
|
||||
const h = parse(rawHours);
|
||||
if (!h) 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 day = DAYS[now.getDay()];
|
||||
const range = h[day];
|
||||
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();
|
||||
|
||||
// Midnight-crossing: close <= open means the range spans past 24:00.
|
||||
if (closeM <= openM) {
|
||||
if (nowM >= openM) return { isOpen: true, kind: 'open', closesAt: range[1] };
|
||||
// before open today — maybe it's open from yesterday's range
|
||||
const yestKey = DAYS[(now.getDay() + 6) % 7];
|
||||
const yest = h[yestKey];
|
||||
if (Array.isArray(yest)) {
|
||||
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] };
|
||||
}
|
||||
}
|
||||
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] };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Refactor (IMPROVE)
|
||||
|
||||
- Extraer `findNextOpen` como `export` y reusarlo en `apps/frontend/src/utils/hours.js` (vía copia local por ahora — son proyectos independientes). No mover al `apps/API/` para no introducir regresiones en el parser OSM, que ya está en producción.
|
||||
- Añadir `coverage` config al `jest.config.js` para exigir 100% en `src/hours.js` (es un módulo puro, sin dependencias).
|
||||
|
||||
**Verificación (regla de evidence):**
|
||||
```bash
|
||||
npm test --workspace=farma-clic-backend -- --testPathPattern=hours
|
||||
# Esperado: 10/10 tests pasados, coverage del nuevo módulo = 100%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 2 — Exponer el estado calculado en los endpoints públicos
|
||||
|
||||
> **Objetivo:** que `/api/medicines/:nregistro/pharmacies` y `/api/pharmacies` devuelvan `is_open` y `is_24h` precalculados, basados en la hora del **servidor**. Esto fija una única fuente de verdad y elimina el bug del frontend con `24:00`.
|
||||
|
||||
### 2.1 Tests primero (RED)
|
||||
|
||||
**Ampliar `apps/backend/__tests__/server.test.js`** (o crear `pharmacy-hours-endpoint.test.js`):
|
||||
|
||||
```
|
||||
GET /api/medicines/:nregistro/pharmacies
|
||||
✓ when DB has pharmacy with opening_hours = 24/7 JSON
|
||||
→ response[0].is_open === true, is_24h === true
|
||||
✓ when DB has pharmacy with closed day
|
||||
→ response[0].is_open === false, is_24h === false
|
||||
✓ when DB has pharmacy with null opening_hours
|
||||
→ response[0].is_open === null, is_24h === false
|
||||
```
|
||||
|
||||
Mockear `userDbAll` o sembrar una BD en memoria antes de cada test (ya se hace en `server.test.js` — seguir el mismo patrón).
|
||||
|
||||
### 2.2 Implementación (GREEN)
|
||||
|
||||
En `apps/backend/server.js`, en ambos endpoints (`/api/medicines/:id/pharmacies` y `/api/products/:source/:id/pharmacies` y `/api/pharmacies`), mapear las filas para añadir dos campos:
|
||||
|
||||
```js
|
||||
import { isOpenNow, isAlwaysOpen } from './src/hours.js';
|
||||
|
||||
// tras obtener `pharmacies`:
|
||||
const enriched = pharmacies.map(p => {
|
||||
let isOpen = null;
|
||||
let is24h = false;
|
||||
if (p.opening_hours) {
|
||||
const status = isOpenNow(p.opening_hours);
|
||||
isOpen = status ? status.isOpen : null;
|
||||
is24h = isAlwaysOpen(p.opening_hours);
|
||||
}
|
||||
return { ...p, is_open: isOpen, is_24h: is24h };
|
||||
});
|
||||
res.json(enriched);
|
||||
```
|
||||
|
||||
Crear un helper local `enrichPharmacy(row)` para no duplicarlo tres veces.
|
||||
|
||||
### 2.3 Refactor (IMPROVE)
|
||||
|
||||
- Si el rendimiento fuera problema (no lo es con <1000 filas), cachear `isOpenNow` con un TTL de 60s por minuto actual. Por ahora se calcula en cada request; dejar un comentario `// TODO(cache): si el dataset crece, cache por minuto`.
|
||||
|
||||
**Verificación:**
|
||||
```bash
|
||||
npm test --workspace=farma-clic-backend
|
||||
# Esperado: tests existentes verdes + los nuevos verdes
|
||||
# Manual: curl a /api/medicines/<nregistro>/pharmacies y verificar is_open en JSON
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 3 — Soporte nativo de farmacias 24h en admin y selector de horarios
|
||||
|
||||
> **Objetivo:** que el admin pueda marcar una farmacia como "24h" con un toggle que rellene los 7 días con `00:00–24:00`, y que el editor manual respete el shape exacto del parser OSM.
|
||||
|
||||
### 3.1 Tests primero (RED)
|
||||
|
||||
**`apps/frontend/src/components/admin/PharmacyManagement.test.jsx`** (nuevo, con `@testing-library/react` si está disponible, si no: tests de unidad sobre los helpers puros)
|
||||
|
||||
Tests del helper puro `hoursToDraft(raw)` y `draftToHours(draft)` (extraídos de `PharmacyManagement.jsx`):
|
||||
|
||||
```
|
||||
hoursToDraft
|
||||
✓ 24/7 JSON → all 7 days with closed: false, open: '00:00', close: '24:00'
|
||||
✓ normal hours → mirror
|
||||
✓ null → all days with closed: true, open: '09:00', close: '21:00' (default)
|
||||
|
||||
draftToHours
|
||||
✓ 24/7 draft (all days closed:false, 00:00, 24:00) → all 7 days ["00:00","24:00"]
|
||||
✓ mixed → matches input
|
||||
✓ all closed → null
|
||||
```
|
||||
|
||||
### 3.2 Refactor del admin (IMPROVE para testabilidad)
|
||||
|
||||
1. Mover `emptyHoursDraft`, `hoursToDraft`, `draftToHours` a `apps/frontend/src/utils/hours.js` (ya existe) y exportarlos.
|
||||
2. En `PharmacyManagement.jsx`, importar desde el helper. Esto los hace testables sin renderizar el componente.
|
||||
3. Añadir **toggle "24 horas"** encima del fieldset de horarios. Estado: `is24h`. Cuando se active, poblar `hoursDraft` con `00:00/24:00` en todos los días y `closed: false`. Cuando se desactive, volver a `emptyHoursDraft()` (pregunte `confirm()` antes de descartar).
|
||||
4. CSS: el toggle debe ser visible y mostrar el estado actual. Estilo coherente con el resto del admin (`.hours-row--always-open`).
|
||||
|
||||
### 3.3 Verificación
|
||||
|
||||
```bash
|
||||
npm test --workspace=farma-clic-frontend
|
||||
# Esperado: tests verdes del helper + sin regresión en PharmacyList
|
||||
# Manual: crear farmacia 24h desde admin → ver en /api/pharmacies que is_24h=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 4 — Filtro "Abiertas Ahora" en la vista pública
|
||||
|
||||
> **Objetivo:** un toggle justo al lado del botón "Ordenar por distancia". Cuando esté activo, la lista y el mapa solo muestran farmacias con `is_open === true` (o `is_24h === true`). Si una farmacia no tiene horarios, se muestra siempre (no se penaliza al admin por no haber editado).
|
||||
|
||||
### 4.1 Tests primero (RED)
|
||||
|
||||
**`apps/frontend/src/views/PublicView.test.jsx`** (nuevo si no existe) o tests sobre un hook puro extraído.
|
||||
|
||||
Refactor sugerido: extraer un hook `usePharmacyFilters(pharmacies, { sortByDistance, userPosition, openNow })` que devuelva la lista filtrada y ordenada. Eso permite testearlo sin renderizar.
|
||||
|
||||
```
|
||||
usePharmacyFilters
|
||||
✓ openNow=false → todas las farmacias
|
||||
✓ openNow=true → solo las con is_open === true
|
||||
✓ openNow=true + pharmacy con opening_hours=null → incluida (no se filtra)
|
||||
✓ openNow=true + is_24h=true → incluida
|
||||
✓ sortByDistance=true → orden por km ascendente
|
||||
✓ sortByDistance=true sin userPosition → orden estable (no rompe)
|
||||
✓ sortByDistance=true + pharmacy sin lat/lng → al final
|
||||
```
|
||||
|
||||
### 4.2 Implementación (GREEN)
|
||||
|
||||
**`apps/frontend/src/views/PublicView.jsx`**
|
||||
|
||||
1. Nuevo state: `const [openNow, setOpenNow] = useState(false);`
|
||||
2. Nuevo botón en `.pharmacy-controls` (donde está "Ordenar por distancia"):
|
||||
```jsx
|
||||
<button
|
||||
className={`open-now-toggle ${openNow ? 'active' : ''}`}
|
||||
onClick={() => setOpenNow(o => !o)}
|
||||
aria-pressed={openNow}
|
||||
>
|
||||
{openNow ? '🟢 Abiertas ahora' : '⏱ Mostrar solo abiertas ahora'}
|
||||
</button>
|
||||
```
|
||||
3. Mover la lógica de `displayedPharmacies` al nuevo hook `usePharmacyFilters(pharmacies, { sortByDistance, userPosition, openNow })`.
|
||||
4. Pasar `displayedPharmacies` a `<PharmacyMap />` y `<PharmacyList />` igual que ahora.
|
||||
5. Si el filtro deja 0 resultados: mostrar mensaje "Ninguna farmacia abierta ahora — desactiva el filtro para ver todas".
|
||||
|
||||
### 4.3 Visualización del badge 24h
|
||||
|
||||
En `apps/frontend/src/components/PharmacyList.jsx`, dentro de `PharmacyCard`, añadir (cuando `pharmacy.is_24h === true`):
|
||||
```jsx
|
||||
<span className="pharmacy-badge pharmacy-badge--24h" aria-label="Abierta 24 horas">24h</span>
|
||||
```
|
||||
al lado del nombre, antes de la distancia.
|
||||
|
||||
En el popup de `PharmacyMap.jsx` (`apps/frontend/src/components/PharmacyMap.jsx`):
|
||||
```jsx
|
||||
{pharmacy.is_24h && <span className="map-badge-24h">24h</span>}
|
||||
```
|
||||
|
||||
### 4.4 Verificación
|
||||
|
||||
```bash
|
||||
npm test --workspace=farma-clic-frontend
|
||||
# Manual:
|
||||
# 1. Crear 2 farmacias: una 9-14h (cerrada ahora si son las 17h), otra 24h
|
||||
# 2. Buscar un medicamento enlazado a las dos
|
||||
# 3. Pulsar "Abiertas ahora" → solo aparece la 24h
|
||||
# 4. Desactivar filtro → aparecen las dos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 5 — i18n de los labels de horario
|
||||
|
||||
> **Objetivo:** mover los strings hardcoded de `apps/frontend/src/utils/hours.js` ("Abierto · Cierra a las X", "Cerrado · Abre mañana…") a los locales `es.js` y `ca.js`. Idem para el nuevo "Abiertas ahora".
|
||||
|
||||
### 5.1 Tests / Tareas
|
||||
|
||||
1. Añadir claves en `apps/frontend/src/i18n/locales/es.js`:
|
||||
```js
|
||||
'pharmacy.openNow': 'Abierto · Cierra a las {{time}}',
|
||||
'pharmacy.closedAllDay': 'Cerrado',
|
||||
'pharmacy.opensAt': 'Cerrado · Abre a las {{time}}',
|
||||
'pharmacy.opensTomorrow': 'Cerrado · Abre mañana a las {{time}}',
|
||||
'pharmacy.opensDay': 'Cerrado · Abre el {{day}} a las {{time}}',
|
||||
'pharmacy.alwaysOpen': 'Abierto 24h',
|
||||
'pharmacy.filterOpenNow': 'Mostrar solo abiertas ahora',
|
||||
'pharmacy.filterOpenNowActive': 'Solo abiertas ahora',
|
||||
'pharmacy.badge24h': '24h',
|
||||
'pharmacy.filterNoResults': 'Ninguna farmacia abierta ahora. Desactiva el filtro para ver todas.'
|
||||
```
|
||||
2. Replicar en `ca.js` con las traducciones catalanas correspondientes.
|
||||
3. Refactor de `getOpenStatus()` para que devuelva `{ status, labelKey, labelParams }` en vez de strings listos, y que `PharmacyList.jsx` los pase por `t()`. Mantener compatibilidad: si no se puede traducir, devolver el string por defecto (defensivo).
|
||||
|
||||
### 5.2 Verificación
|
||||
|
||||
```bash
|
||||
npm test --workspace=farma-clic-frontend
|
||||
# Manual: cambiar idioma a catalán (ca) y verificar badges
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 6 — Verificación end-to-end (cierre del plan)
|
||||
|
||||
> Antes de declarar el plan completo, ejecutar el set de verificaciones del repo (regla "evidence-based completion" — sin evidencia, no se afirma éxito).
|
||||
|
||||
### 6.1 Backend
|
||||
|
||||
```bash
|
||||
# Linter
|
||||
cd apps/backend && npx eslint .
|
||||
|
||||
# Tests
|
||||
npm test --workspace=farma-clic-backend
|
||||
# Cobertura ≥ 80% en modules nuevos:
|
||||
# apps/backend/src/hours.js → 100%
|
||||
# apps/backend/__tests__/hours.test.js → 100%
|
||||
# endpoints pharmacy hours tests → ≥ 80%
|
||||
```
|
||||
|
||||
### 6.2 Frontend
|
||||
|
||||
```bash
|
||||
# Linter
|
||||
cd apps/frontend && npx eslint .
|
||||
|
||||
# Tests
|
||||
npm test --workspace=farma-clic-frontend
|
||||
# Cobertura ≥ 80% en:
|
||||
# apps/frontend/src/utils/hours.js (tras mover helpers de admin) → 100%
|
||||
# apps/frontend/src/views/PublicView.jsx → ≥ 80%
|
||||
# apps/frontend/src/components/PharmacyList.jsx → ≥ 80%
|
||||
# apps/frontend/src/components/admin/PharmacyManagement.jsx → ≥ 80%
|
||||
```
|
||||
|
||||
### 6.3 Integración real (Docker)
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
docker compose exec backend npm run seed # crea 3 farmacias: 24h, 9-21h, fines de semana
|
||||
curl -s http://localhost:3001/api/pharmacies | jq '.[0] | {name, is_open, is_24h}'
|
||||
# Esperado: 24h farmacia con is_open=true, is_24h=true
|
||||
```
|
||||
|
||||
### 6.4 E2E manual (smoke)
|
||||
|
||||
1. Abrir `http://localhost:4000`, buscar "ibuprofeno", seleccionar.
|
||||
2. Verificar: las 3 farmacias aparecen con su badge de estado correcto.
|
||||
3. Pulsar "Mostrar solo abiertas ahora" → solo las abiertas.
|
||||
4. Pulsar "Ordenar por distancia" → reordena, badge 24h visible.
|
||||
5. Abrir el popup del mapa de la 24h → etiqueta "24h" visible.
|
||||
|
||||
### 6.5 Regresión
|
||||
|
||||
- `apps/backend/__tests__/opening-hours-osm.test.js` → sigue verde (no tocamos el parser).
|
||||
- `apps/backend/__tests__/server.test.js` → sigue verde; los nuevos endpoints se añaden sin romper los existentes.
|
||||
- `apps/frontend/src/components/PharmacyList.jsx` actual: si `is_24h` es `undefined` (respuesta legacy de la BD sin enriquecer), debe renderizarse igual que antes (compatibilidad).
|
||||
|
||||
---
|
||||
|
||||
## Orden de implementación recomendado
|
||||
|
||||
| Paso | Tiempo estimado | Bloquea |
|
||||
|------|----------------|---------|
|
||||
| 1. Helper backend `src/hours.js` (TDD) | 1.5 h | Pasos 2 y 4 |
|
||||
| 2. Enriquecer endpoints públicos (TDD) | 1 h | Paso 4 (frontend necesita el campo) |
|
||||
| 3. Admin 24h (refactor + test) | 1 h | — (paralelo a 4) |
|
||||
| 4. Filtro "Abiertas ahora" + badge 24h (TDD) | 2 h | — |
|
||||
| 5. i18n (es + ca) | 0.5 h | — |
|
||||
| 6. Verificación + cobertura | 0.5 h | Cierre |
|
||||
| **Total** | **~6.5 h** | |
|
||||
|
||||
Pasos 1 y 3 pueden hacerse en paralelo. Paso 5 puede entramparse con 4.
|
||||
|
||||
---
|
||||
|
||||
## Riesgos y mitigaciones
|
||||
|
||||
| Riesgo | Mitigación |
|
||||
|--------|-----------|
|
||||
| `parseOsmOpeningHours` cambia en producción y rompe el shape | Tests existentes en `opening-hours-osm.test.js` ya cubren los casos. No tocar. |
|
||||
| Hora del servidor difiere del navegador del usuario | Devolver `server_now` en la respuesta (`{ is_open, server_now, server_tz }`) para que la UI pueda mostrar "estado según la hora del servidor". Si en el futuro se quiere client-side, aceptar `?at=<iso>` y calcular contra ese timestamp. |
|
||||
| Performance: 1000 farmacias × 7 días de parseo por request | Trivial. Si crece, cachear por minuto (ver 2.3). |
|
||||
| `24:00` no es `HH:mm` válido ISO y rompe parsers terceros | Mantener el shape interno pero, al exportar, escribir un campo derivado `is_24h` (booleano) en el JSON público. El shape interno sigue siendo el mismo. |
|
||||
| i18n incompleta en ca.js | Mantener strings en `es.js` como fallback. Si una clave falta en `ca.js`, el `useTranslation` ya cae al idioma por defecto. |
|
||||
|
||||
---
|
||||
|
||||
## Archivos a crear / modificar (resumen)
|
||||
|
||||
**Crear:**
|
||||
- `apps/backend/src/hours.js`
|
||||
- `apps/backend/__tests__/hours.test.js`
|
||||
- `apps/backend/__tests__/pharmacy-hours-endpoint.test.js`
|
||||
- `apps/frontend/src/views/PublicView.test.jsx` (o `usePharmacyFilters.test.js` si se extrae el hook)
|
||||
- `apps/frontend/src/components/admin/PharmacyManagement.test.jsx` (si testing-library está disponible; si no, tests sobre los helpers puros)
|
||||
|
||||
**Modificar:**
|
||||
- `apps/backend/server.js` — enriquecer `pharmacies` en 3 endpoints; importar `src/hours.js`
|
||||
- `apps/frontend/src/views/PublicView.jsx` — state `openNow`, hook de filtros, botón en `.pharmacy-controls`
|
||||
- `apps/frontend/src/components/PharmacyList.jsx` — badge 24h, pasar por i18n
|
||||
- `apps/frontend/src/components/PharmacyMap.jsx` — badge 24h en popup
|
||||
- `apps/frontend/src/components/admin/PharmacyManagement.jsx` — toggle 24h, importar helpers
|
||||
- `apps/frontend/src/utils/hours.js` — mover `emptyHoursDraft`/`hoursToDraft`/`draftToHours` desde admin; añadir `isAlwaysOpen`; cambiar `getOpenStatus` para devolver labelKey/params
|
||||
- `apps/frontend/src/i18n/locales/es.js` — nuevas claves
|
||||
- `apps/frontend/src/i18n/locales/ca.js` — nuevas claves
|
||||
|
||||
**No tocar:**
|
||||
- `apps/API/opening-hours-osm.js` (parser OSM en producción)
|
||||
- `apps/backend/farmacias-webhook-import.js` (ya parsea correctamente)
|
||||
|
||||
---
|
||||
|
||||
## Preguntas abiertas (a resolver antes de implementar)
|
||||
|
||||
1. **¿Soportar farmacias que cruzan medianoche?** (ej. `mon ["22:00","02:00"]`). El shape actual lo permite pero `getOpenStatus` actual no lo trata. **Propuesta:** sí, implementar el caso en el helper del backend (tests ya incluidos en 1.1).
|
||||
2. **¿Borrar `getOpenStatus` del frontend o mantenerlo como fallback?** **Propuesta:** mantenerlo, pero hacer que la UI use `pharmacy.is_open` y `pharmacy.is_24h` cuando estén presentes (del backend) y solo caiga al cálculo local si faltan. Sin duplicación de lógica.
|
||||
3. **¿Mostrar farmacia sin horarios cuando el filtro "abiertas ahora" está activo?** **Propuesta:** sí, mostrarlas (asumimos que no sabemos = no filtramos). Documentarlo en la UI.
|
||||
4. **¿Refrescar el estado cada minuto?** (por si la lista queda abierta mucho tiempo y cambia el estado). **Propuesta:** no por ahora; si la lista se refresca al cambiar de medicamento, basta. Si se observa uso prolongado sin refresco, añadir `setInterval` de 60s para recalcular.
|
||||
|
||||
Cuando confirmes el plan (o pidas ajustes) lo paso a implementación siguiendo TDD estricto.
|
||||
Reference in New Issue
Block a user