Documentos + Homepage rediseñada
Build & Push Docker Images / test-backend (push) Successful in 24s
Build & Push Docker Images / test-frontend (push) Successful in 23s
Build & Push Docker Images / build-backend (push) Successful in 2m22s
Build & Push Docker Images / build-frontend (push) Successful in 57s

This commit is contained in:
Antoni Nuñez Romeu
2026-06-26 16:59:20 +02:00
parent 14e9c16310
commit 2c1b3cfca6
36 changed files with 4665 additions and 372 deletions
+209
View File
@@ -0,0 +1,209 @@
# FarmaFinder — TSI Barcode Scanning + Capacitor 8 Upgrade
## Overview
Two sequential phases:
1. **Phase A** — Upgrade Capacitor 6 → 8 (prerequisite for latest barcode scanner plugin)
2. **Phase B** — Implement real TSI barcode scanning with manual CIP entry fallback
---
## Phase A: Upgrade Capacitor 6 → 8
### A1. Upgrade Capacitor packages (root)
```bash
npm install @capacitor/cli@latest @capacitor/core@latest \
@capacitor/android@latest @capacitor/ios@latest \
@capacitor/app@latest @capacitor/splash-screen@latest \
@capacitor/status-bar@latest
```
This bumps all `@capacitor/*` from `^6.x` to `^8.x` in `package.json`.
### A2. Update `android/variables.gradle`
Replace all 13 version variables to meet Capacitor 8 minimums:
| Variable | Current | New |
|---|---|---|
| `minSdkVersion` | 22 | **24** |
| `compileSdkVersion` | 34 | **36** |
| `targetSdkVersion` | 34 | **36** |
| `androidxActivityVersion` | 1.8.0 | **1.11.0** |
| `androidxAppCompatVersion` | 1.6.1 | **1.7.1** |
| `androidxCoordinatorLayoutVersion` | 1.2.0 | **1.3.0** |
| `androidxCoreVersion` | 1.12.0 | **1.17.0** |
| `androidxFragmentVersion` | 1.6.2 | **1.8.9** |
| `coreSplashScreenVersion` | 1.0.1 | **1.2.0** |
| `androidxWebkitVersion` | 1.9.0 | **1.14.0** |
| `junitVersion` | 4.13.2 | 4.13.2 (same) |
| `androidxJunitVersion` | 1.1.5 | **1.3.0** |
| `androidxEspressoCoreVersion` | 3.5.1 | **3.7.0** |
| `cordovaAndroidVersion` | 10.1.1 | **14.0.1** |
### A3. Update `ios/App/Podfile`
- Line 3: Change `platform :ios, '13.0'``platform :ios, '15.0'`
### A4. Update `android/app/src/main/AndroidManifest.xml`
- Add `density` to `android:configChanges` on line 13:
```
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|density"
```
### A5. Update `android/build.gradle` (Gradle plugin)
- Update Android Gradle Plugin from `8.2.1` → `8.13.0`
- Update Google Services plugin from `4.4.0` → `4.4.4`
- Update Gradle wrapper to `8.14.3` in `android/gradle/wrapper/gradle-wrapper.properties`
### A6. Run Capacitor sync
```bash
npx cap sync
```
### A7. Verify build
- Open `android/` in Android Studio — confirm Gradle sync succeeds
- Open `ios/App` in Xcode — confirm pod install succeeds and build compiles
---
## Phase B: Real TSI Barcode Scanning
### B1. Install barcode scanning plugin
From `frontend/`:
```bash
npm install @capacitor-mlkit/barcode-scanning@^8.1.0
npm install barcode-detector # web polyfill for PWA browser fallback
```
From root:
```bash
npx cap sync
```
**Plugin:** `@capacitor-mlkit/barcode-scanning` v8.1.0 — Google ML Kit engine, 91K+ weekly npm downloads, supports Code 128 (TSI physical card barcode) and QR Code (virtual TSI card).
### B2. Native manifest verification (no changes expected)
| Platform | File | Required | Status |
|---|---|---|---|
| Android | `AndroidManifest.xml:44` | `CAMERA` permission | Already present |
| iOS | `Info.plist:52-53` | `NSCameraUsageDescription` | Already present |
### B3. Rewrite `frontend/src/views/ScannerView.jsx`
**Remove:**
- `MOCK_TSI_CARDS` array and all mock card state/UI
- `navigator.mediaDevices.getUserMedia()` camera logic (lines 40-73)
- Auto-scan `setTimeout` simulation (lines 98-105)
- Simulated scan fallback panel (lines 170-198)
- `selectedMockCard` state
**Add:**
- Import `BarcodeScanner`, `BarcodeFormat`, `LensFacing` from `@capacitor-mlkit/barcode-scanning`
- Import `Capacitor` from `@capacitor/core` (platform detection)
- Import `barcode-detector/polyfill` at top (web fallback)
**New scan flow:**
```
1. Component mounts → show "Start Scanning" button + manual CIP input
2. User taps "Start Scanning":
a. Check BarcodeScanner.isSupported() → error if not
b. Check/request camera permission via BarcodeScanner.checkPermissions() / requestPermissions()
c. Call BarcodeScanner.scan({ formats: [BarcodeFormat.Code128, BarcodeFormat.QrCode], autoZoom: true })
d. Extract barcodes[0].rawValue (NOTE: can be undefined per v8 breaking change — handle gracefully)
e. Validate CIP format: /^[A-Z0-9]{16}$/i
f. If valid → fetchPrescriptions(cip) → show prescriptions
g. If invalid → show error "Invalid card barcode. Try manual entry."
3. Manual CIP input:
- Text input field with placeholder "Enter CIP code manually"
- "Submit" button → validates format → fetchPrescriptions(cip)
4. PWA/browser fallback:
- Detect: !Capacitor.isNativePlatform()
- Show message: "Barcode scanning requires the mobile app. Enter your CIP code manually below."
- Show only the manual CIP input (no scan button)
```
**Phases (simplified):**
- `idle` — initial state, scan button + manual input visible
- `scanning` — ML Kit native UI active (camera takes over)
- `prescriptions` — results display (keep existing prescription card UI)
- `error` — permission denied / unsupported / invalid barcode
**Keep unchanged:**
- `playBeep()` function
- `fetchPrescriptions(cip)` function
- `handlePickPrescription(rx)` function
- `handleBack()` function
- Prescription display UI (lines 201-249)
- `onSelectMedicine` callback prop
### B4. Rewrite `frontend/src/views/ScannerView.css`
**Remove:**
- `.scanner-viewport-wrap`, `.scanner-camera-container`, `.scanner-video`, `.scanner-frame`, `.scanner-laser`, `.scanner-hint` (old camera overlay)
- `.scanner-simulate-panel`, `.mock-cards-list`, `.mock-card-btn`, `.simulate-scan-btn`, `.simulate-label`, `.mock-card-icon`, `.mock-card-info` (mock card panel)
- `.scanner-placeholder` with spinner (old loading state)
**Add:**
- `.scan-start-btn` — large centered button with camera icon, teal background (`#0f766e`), full-width
- `.manual-cip-section` — input group with text field + submit button
- `.cip-input` — text input styled to match existing dark theme, monospace font
- `.cip-submit-btn` — teal button matching scan button style
- `.pwa-notice` — info banner for browser users
- `.scan-error-panel` — error state with icon and retry button
### B5. No changes to other files
- `HomeView.jsx` — "Scan TSI Card" button still navigates to ScannerView
- `PublicView.jsx` — screen routing and `onSelectMedicine` handoff unchanged
- `backend/server.js` — user handles `/api/tsi/:cip/prescriptions`
- `capacitor.config.json` — no scanner-specific config needed
---
## Files Modified (summary)
| File | Phase | Change |
|---|---|---|
| `package.json` | A1 | Bump all `@capacitor/*` to ^8.x |
| `frontend/package.json` | B1 | Add `@capacitor-mlkit/barcode-scanning@^8.1.0` + `barcode-detector` |
| `android/variables.gradle` | A2 | Update all 13 SDK/library versions |
| `ios/App/Podfile` | A3 | iOS deployment target 13.0 → 15.0 |
| `android/app/src/main/AndroidManifest.xml` | A4 | Add `density` to configChanges |
| `android/build.gradle` | A5 | AGP 8.2.1 → 8.13.0, Google Services 4.4.0 → 4.4.4 |
| `android/gradle/wrapper/gradle-wrapper.properties` | A5 | Gradle wrapper → 8.14.3 |
| `frontend/src/views/ScannerView.jsx` | B3 | Full rewrite — real barcode scanning + manual input |
| `frontend/src/views/ScannerView.css` | B4 | Remove old camera/mock styles, add new scan UI |
---
## Known Issues
- **Android NullPointerException bug** (GitHub capawesome-team/capacitor-mlkit #160/#324) — affects `startScan()` in v8.0.1. Workaround: use `scan()` method (built-in native UI) instead of `startScan()` (custom WebView mode). Plan uses `scan()`, so this shouldn't hit us.
- **`rawValue` can be undefined** in v8.x — handle with null check before CIP validation.
- **Capacitor 8 requires Xcode 26+** — must have latest Xcode installed.
- **Gradle 8.14.3 requires JDK 21+** — confirm JDK 21 is available.
- **`@capacitor-mlkit/barcode-scanning` v6.x.x is deprecated** — this is why we upgrade to Capacitor 8 first (v8.x.x plugin requires Cap 8).
---
## Implementation Order
| Step | Phase | Estimated Time |
|---|---|---|
| A1-A6 | Capacitor upgrade | ~30 min |
| A7 | Verify build (Android + iOS) | ~30 min |
| B1 | Install barcode plugin | ~5 min |
| B3 | Rewrite ScannerView.jsx | ~2 hrs |
| B4 | Update ScannerView.css | ~30 min |
| B5 | Test on Android device | ~30 min |
| B5 | Test on iOS device | ~30 min |
Total: ~4-5 hours
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+709
View File
@@ -0,0 +1,709 @@
# Memoria tecnica FarmaFinder
## 1. Resumen ejecutivo
FarmaFinder es una aplicacion web/PWA para buscar medicamentos en la base oficial de CIMA (AEMPS), localizar farmacias que los comercializan y administrar el catalogo de farmacias y sus relaciones con medicamentos. La aplicacion tambien incluye perfil de usuario con ubicacion guardada, geocodificacion, ordenacion por distancia, estados de apertura de farmacias, notificaciones push para medicamentos y un panel de administracion para altas/bajas/importacion de farmacias.
La base funcional actual esta repartida en cuatro capas principales:
- `frontend/`: interfaz React + Vite, tambien preparada para PWA y empaquetado Capacitor.
- `backend/`: API Express con sesion, autenticacion, persistencia SQLite/PG y notificaciones push.
- `API/`: helpers de importacion y normalizacion de farmacias desde fuentes externas.
- `android/` e `ios/`: shells nativos generados por Capacitor.
## 2. Stack tecnologico
### Frontend
- React 18
- Vite 5
- Leaflet + React Leaflet para el mapa
- Vitest + Testing Library + JSDOM para tests de UI
- PWA con `vite-plugin-pwa`
- Capacitor 6 para envoltorio nativo
### Backend
- Node.js en modo ESM
- Express 4
- SQLite como almacenamiento base local
- PostgreSQL opcional para usuarios y sesiones cuando `PG_URL` esta definido
- Redis para cache de CIMA
- `express-session` para sesion de usuario
- `bcrypt` para hash de contrasenas
- `web-push` para notificaciones push web
- `express-rate-limit` para proteccion de endpoints sensibles
### Integraciones externas
- CIMA API de la AEMPS para busqueda y detalle de medicamentos
- Nominatim / OpenStreetMap para geocodificacion
- OpenStreetMap Overpass u otras fuentes JSON para importacion de farmacias
- Webhooks externos tipo n8n para importacion de farmacias
## 3. Estructura del repositorio
- `frontend/`: SPA principal
- `backend/`: API principal y scripts de mantenimiento
- `API/`: libreria de importacion de farmacias desde fuentes externas
- `scraper/`: utilidades de scraping con Puppeteer
- `android/` y `ios/`: proyectos nativos Capacitor
- `docker-compose.yml`: entorno completo con backend, frontend, Redis y PostgreSQL
## 4. Arquitectura funcional
### Flujo publico
1. El usuario escribe un medicamento en el buscador.
2. El frontend llama a `GET /api/medicines/search`.
3. El backend consulta CIMA, filtra resultados y los cachea en Redis.
4. Al seleccionar un medicamento, el frontend llama a `GET /api/medicines/:id/pharmacies`.
5. El backend devuelve farmacias enlazadas en SQLite/PG con precio, stock, coordenadas y horario.
6. La UI permite ordenar por distancia si el usuario tiene ubicacion guardada o concede geolocalizacion.
### Flujo de administracion
1. Un admin inicia sesion.
2. El panel permite gestionar farmacias, buscar medicamentos en CIMA e importar farmacias externas.
3. El admin enlaza medicamentos a farmacias con precio y stock.
4. Al crear un nuevo enlace de farmacia-medicamento, el backend dispara notificaciones push a usuarios suscritos.
### Flujo de perfil y ubicacion
1. Un usuario autenticado puede editar su perfil.
2. Puede guardar direccion y coordenadas manualmente.
3. Tambien puede geocodificar su direccion via Nominatim o usar la geolocalizacion del navegador.
4. Esa ubicacion se usa en el ordenamiento por distancia en la vista publica.
## 5. Frontend
### Punto de entrada
- `frontend/src/main.jsx` monta `<App />` en `#root`.
- Se inicializa el shell nativo con `initNativeShell()`.
### Componente raiz
`frontend/src/App.jsx` controla tres vistas:
- `public`
- `admin`
- `profile`
Tambien gestiona:
- comprobacion de sesion con `GET /api/auth/check`
- apertura/cierre del modal de login
- cierre de sesion con `POST /api/auth/logout`
- panel de notificaciones guardadas
### Vistas principales
#### PublicView
`frontend/src/views/PublicView.jsx`
Responsabilidades:
- busqueda de medicamentos con debounce
- consulta de farmacias asociadas al medicamento
- mapa de farmacias
- lista de farmacias con precio, stock, horario y notificaciones
- ordenacion por distancia usando ubicacion guardada o geolocalizacion del navegador
Caracteristicas clave:
- Si el usuario tiene coordenadas en el perfil, se priorizan sobre la geolocalizacion del navegador.
- La lista y el mapa usan la misma ordenacion.
- Si no hay coordenadas o el navegador falla, se muestra un mensaje de error contextual.
#### AdminView
`frontend/src/views/AdminView.jsx`
Responsabilidades:
- autenticar acceso al panel
- mostrar tabs de:
- farmacias
- medicamentos
- enlace farmacia-medicamento
- logout de admin
#### ProfileView
`frontend/src/views/ProfileView.jsx`
Responsabilidades:
- editar direccion y coordenadas del usuario
- geocodificar la direccion con `/api/geocode`
- obtener ubicacion actual del dispositivo
- abrir la vista de notificaciones guardadas
- cerrar sesion
## 6. Componentes de UI
### Busqueda y resultados
- `frontend/src/components/SearchBar.jsx`
- `frontend/src/components/MedicineResults.jsx`
- `frontend/src/components/PharmacyList.jsx`
- `frontend/src/components/PharmacyMap.jsx`
#### MedicineResults
- Lista resultados de CIMA.
- Cada card incluye boton de notificaciones para ese medicamento.
- Al pulsar la card se abre el detalle de farmacias asociadas.
#### PharmacyList
- Muestra farmacias enlazadas al medicamento.
- Incluye:
- distancia al usuario si existe ubicacion
- estado de apertura calculado a partir de `opening_hours`
- direccion
- telefono
- precio
- stock
- boton de notificacion por farmacia cuando el stock esta agotado
#### PharmacyMap
- Muestra farmacias sobre Leaflet.
- Usa coordenadas de cada farmacia para ubicarlas en el mapa.
### Admin components
- `frontend/src/components/admin/PharmacyManagement.jsx`
- `frontend/src/components/admin/MedicineManagement.jsx`
- `frontend/src/components/admin/PharmacyMedicineLink.jsx`
- `frontend/src/components/admin/LoginForm.jsx`
#### PharmacyManagement
- Lista farmacias existentes.
- Permite crear, editar y borrar farmacias.
- Integra importacion por:
- webhook externo
- OpenStreetMap / Overpass
- URL de open data JSON/GeoJSON
- Permite filtrar farmacias por radio geografico.
- Permite geocodificar una ciudad para obtener lat/lon/radio de trabajo.
- Gestiona horario semanal estructurado.
#### MedicineManagement
- Busca medicamentos en CIMA desde el panel admin.
- Solo sirve para exploracion y seleccion previa al enlace.
#### PharmacyMedicineLink
- Permite seleccionar farmacia y medicamento.
- Crea o actualiza el enlace con precio y stock.
- Permite editar y eliminar relaciones ya existentes.
## 7. Utilidades de frontend
### Geolocalizacion
`frontend/src/utils/geo.js`
- `haversineKm()`: calcula distancia entre dos puntos GPS.
- `getUserPosition()`: obtiene ubicacion del navegador con validaciones de contexto seguro.
- `formatDistance()`: da formato a metros/kilometros.
### Horarios
`frontend/src/utils/hours.js`
- `getOpenStatus()`: interpreta el horario semanal y devuelve un estado legible.
- Soporta:
- abierto
- cerrado
- abre a una hora concreta
- abre el siguiente dia habil
- El formato almacenado es JSON con claves `sun`, `mon`, `tue`, `wed`, `thu`, `fri`, `sat`.
### Notificaciones
`frontend/src/utils/notifications.js`
- comprueba soporte de Push API y Service Worker
- obtiene la VAPID public key del backend
- registra suscripciones push del navegador
- guarda una huella local en `localStorage`
- permite suscripcion global por medicamento o por medicamento + farmacia
### Service worker
`frontend/src/sw.js`
- precache de assets PWA
- muestra notificaciones al recibir eventos `push`
- navega a la URL asociada al click en la notificacion
## 8. Backend
### Arranque
`backend/server.js` es el nucleo de la API. Expone:
- la aplicacion Express
- `initDatabase()`
- la conexion `db`
Si `NODE_ENV !== test`, inicializa DB y levanta el servidor en `PORT` o `3001`.
### Configuracion basica
- CORS con credenciales
- `express.json()`
- session middleware
- rate limiting en:
- busqueda
- login
- registro
- geocoding
### Persistencia
#### SQLite
Se usa como base local principal:
- farmacias
- relaciones farmacia-medicamento
- subscripciones push
- fallback de usuarios cuando no hay PostgreSQL
#### PostgreSQL opcional
Si existe `PG_URL`, la tabla `users` y el store de sesiones se montan sobre PostgreSQL.
Esto genera una arquitectura mixta:
- SQLite para catalogo/relaciones
- PostgreSQL para usuarios y sesiones, si se configura
### Capa de sesion
- `express-session`
- store SQLite o PostgreSQL segun entorno
- cookie httpOnly con maxAge de 24h
- `trust proxy` configurable con `TRUST_PROXY`
## 9. Modelo de datos
### `pharmacies`
Campos:
- `id`
- `name`
- `address`
- `phone`
- `latitude`
- `longitude`
- `opening_hours`
### `pharmacy_medicines`
Relacion farmacia-medicamento.
Campos:
- `id`
- `pharmacy_id`
- `medicine_nregistro`
- `medicine_name`
- `price`
- `stock`
Restriccion:
- `UNIQUE(pharmacy_id, medicine_nregistro)`
### `users`
Campos actuales:
- `id`
- `username`
- `password_hash`
- `is_admin`
- `address`
- `latitude`
- `longitude`
- `created_at`
### `push_subscriptions`
Subscripciones push globales por medicamento.
Campos:
- `id`
- `medicine_nregistro`
- `medicine_name`
- `endpoint`
- `p256dh`
- `auth`
- `user_id`
- `created_at`
### `push_subscriptions_pharmacy`
Subscripciones push por medicamento y farmacia.
Campos:
- `id`
- `medicine_nregistro`
- `medicine_name`
- `pharmacy_id`
- `endpoint`
- `p256dh`
- `auth`
- `user_id`
- `created_at`
## 10. API backend
### Medicamentos
- `GET /api/medicines/search?q=...`
- busca en CIMA con cache Redis
- devuelve una lista normalizada
- `GET /api/medicines/:medicineId`
- devuelve detalle de un medicamento por `nregistro`
- `GET /api/medicines/:medicineId/pharmacies`
- devuelve farmacias que tienen ese medicamento
### Farmacias
- `GET /api/pharmacies`
- lista completa de farmacias
### Autenticacion
- `POST /api/auth/login`
- `POST /api/auth/register`
- `POST /api/auth/logout`
- `GET /api/auth/check`
### Perfil
- `GET /api/users/me`
- `PUT /api/users/me`
### Geocoding
- `GET /api/geocode?q=...`
- autenticado
- usa Nominatim
- devuelve lat/lon y texto de resultado
- `GET /api/admin/geocode?q=...`
- solo admin
- ademas calcula radio sugerido
### Admin: farmacias
- `POST /api/admin/pharmacies`
- `PUT /api/admin/pharmacies/:id`
- `DELETE /api/admin/pharmacies/:id`
- `POST /api/admin/pharmacies/import-webhook`
- `POST /api/admin/pharmacies/import-external`
### Admin: medicamentos y relaciones
- `GET /api/admin/medicines`
- `GET /api/admin/pharmacies/:pharmacyId/medicines`
- `POST /api/admin/pharmacy-medicines`
- `PUT /api/admin/pharmacy-medicines/:id`
- `DELETE /api/admin/pharmacy-medicines/:id`
### Notificaciones
- `GET /api/notifications/vapid-public-key`
- `POST /api/notifications/subscribe`
- `DELETE /api/notifications/unsubscribe`
- `GET /api/notifications/mine`
- `DELETE /api/notifications/mine`
## 11. Capa de importacion externa
La carpeta `API/` no expone un servidor HTTP propio, sino funciones reutilizables para importar farmacias desde distintas fuentes.
### `API/index.js`
- `fetchPharmaciesExternal(opts)`
- `source = 'osm'`
- `source = 'openData'`
- decide el extractor adecuado segun la fuente
### `API/normalize.js`
- `buildAddressFromOsmTags()`
- `osmElementToPharmacy()`
- normaliza nodos Overpass a un formato comun de farmacia
- parsea horarios OSM si existen
### `API/opening-hours-osm.js`
- parser de un subconjunto de `opening_hours`
- soporta:
- `24/7`
- rangos semanales
- dias multiples
- `off` / `closed`
- franjas partidas resumidas
### `backend/farmacias-webhook-import.js`
- importa datos desde un webhook HTTP
- normaliza campos en espanol o ingles
- acepta ubicacion en distintas variantes
- extrae horarios si vienen en formato OSM
- inserta o salta duplicados con estadisticas
## 12. CIMA y cache
`backend/cima-service.js` implementa la integracion con CIMA.
### Busqueda
- consulta `https://cima.aemps.es/cima/rest/medicamentos`
- usa el parametro `nombre`
- normaliza el resultado a campos internos:
- `id`
- `nregistro`
- `name`
- `active_ingredient`
- `dosage`
- `form`
- `formSimplified`
- `laboratory`
- `prescription`
- `commercialized`
- `generic`
- `photos`
- `docs`
### Detalle
- consulta `GET /medicamento/:nregistro`
- cachea por mas tiempo que la busqueda
### Cache
- Redis guarda resultados de busqueda y detalle
- si CIMA falla, se intenta devolver cache obsoleto si existe
## 13. Notificaciones push
### Estado actual
La app ya soporta notificaciones push web completas.
### Backend
- usa VAPID si `VAPID_PUBLIC_KEY` y `VAPID_PRIVATE_KEY` existen
- si faltan, push queda desactivado con respuesta `503`
- al crear una relacion nueva farmacia-medicamento, se ejecuta `sendPushForMedicine()`
- se deduplican endpoints para evitar notificacion doble global + farmacia
- si el endpoint devuelve `404` o `410`, la suscripcion se limpia
### Frontend
- permite suscripcion global por medicamento
- permite suscripcion por medicamento + farmacia
- el icono de campana refleja el estado local
- la vista `Saved notifications` permite ver y borrar suscripciones guardadas del usuario
## 14. PWA y capa nativa
### PWA
- existe `frontend/src/sw.js`
- hay assets publicos de icono
- la app esta pensada para funcionar como PWA instalable
### Capacitor
- `capacitor.config.json`
- `android/`
- `ios/`
El proyecto esta preparado para empaquetarse como app nativa. La documentacion de `NATIVE.md` indica que no hay una base de codigo separada para Android e iOS; la UI real vive en `frontend/`.
## 15. Scripts y comandos
### Raiz
- `npm run dev`: backend + frontend en paralelo
- `npm run start`: backend + preview frontend
- `npm run install:all`: instala dependencias raiz, backend y frontend
- `npm run build:web`: build de frontend
- `npm run cap:sync`: build web + sync Capacitor
### Backend
- `npm start`
- `npm run dev`
- `npm run seed`
- `npm run create-admin`
- `npm run migrate`
- `npm run import-farmacias`
- `npm test`
### Frontend
- `npm run dev`
- `npm run build`
- `npm run preview`
- `npm run test`
## 16. Docker
`docker-compose.yml` levanta:
- `redis`
- `postgres`
- `backend`
- `frontend`
Puertos:
- frontend: `3000`
- backend: `3001`
Persistencia:
- `backend_data` para la DB SQLite del backend
- `postgres_data` para PostgreSQL
## 17. Variables de entorno
### Backend
- `PORT`
- `SESSION_SECRET`
- `CORS_ORIGIN`
- `FARMACIAS_WEBHOOK_URL`
- `REDIS_HOST`
- `REDIS_PORT`
- `REDIS_PASSWORD`
- `DATABASE_PATH`
- `PG_URL`
- `TRUST_PROXY`
- `VAPID_PUBLIC_KEY`
- `VAPID_PRIVATE_KEY`
- `VAPID_SUBJECT`
- `NODE_ENV`
### App
- en produccion, el frontend debe poder resolver la URL del backend segun despliegue
- en native shell, el manual `NATIVE.md` indica que conviene usar URL absoluta o servir el PWA desplegado
## 18. Seeds, migraciones y administracion
### `backend/seed.js`
- crea tablas basicas
- inserta farmacias y medicamentos de ejemplo
- genera relaciones ficticias usando `EXAMPLE_...`
- actualmente es util para demos, no como dato real
### `backend/create-admin.js`
- crea usuario admin en SQLite o PostgreSQL segun `PG_URL`
- por defecto:
- usuario: `admin`
- password: `admin123`
### `backend/migrate.js`
- migracion para el cambio historico de `medicine_id` a `medicine_nregistro`
- la tabla `medicines` antigua se conserva como referencia si existe
## 19. Tests
### Backend
- `backend/__tests__/server.test.js`
- valida busqueda vacia
- valida login incorrecto
- valida proteccion de rutas admin
- valida creacion de farmacia autenticado
- `backend/__tests__/opening-hours-osm.test.js`
- cubre parser de horarios OSM
### Frontend
- existe configuracion de Vitest
- hay `frontend/src/App.test.jsx`
- la cobertura visible en el repo parece enfocada a smoke tests y utilidades
## 20. Observaciones tecnicas y limites actuales
1. El backend mezcla SQLite y PostgreSQL segun entorno. Esto funciona, pero introduce dos rutas de persistencia para usuarios y sesiones.
2. Los medicamentos no se guardan localmente como catalogo propio; se consumen desde CIMA en tiempo real y se cachean en Redis.
3. Los enlaces farmacia-medicamento dependen de `nregistro` de CIMA, no de un ID interno de medicamentos.
4. Las notificaciones push son web push; en shells nativos siguen la logica del navegador/PWA, no una integracion FCM/APNs independiente.
5. La importacion de farmacias depende de la calidad de la fuente externa y de la normalizacion de campos.
6. El seed actual contiene datos de ejemplo y relaciones ficticias; no representa datos de produccion.
## 21. Conclusiones
El estado actual de FarmaFinder es el de una plataforma funcional orientada a consulta y administracion de farmacias y medicamentos, con un backend ya bastante consolidado: autentica usuarios, controla roles, persiste relaciones, geocodifica, importa fuentes externas, cachea CIMA y dispara push notifications. La parte de frontend acompana ese backend con un flujo publico de busqueda, un perfil con ubicacion y un panel admin operativo.
Si quieres, el siguiente paso puede ser convertir esta memoria en una version formal de entrega, con estilo academico o empresarial, o bien en un documento mas tecnico con diagramas de arquitectura y secuencia.
## 22. Next steps propuestos
El siguiente paso funcional natural para FarmaFinder seria incorporar el escaneo de la TSI, de forma que el usuario pueda identificar rapidamente su perfil asistencial y, a partir de ahi, cruzar su necesidad con el stock real disponible en farmacias.
### Objetivo
- Capturar la informacion de la tarjeta sanitaria individual mediante escaneo.
- Asociar esa informacion al perfil del usuario de manera segura.
- Recuperar o inferir los medicamentos relevantes para el usuario.
- Contrastar esos medicamentos con el stock de farmacias cercanas.
- Permitir actuar si no hay stock disponible, por ejemplo:
- reservar una farmacia concreta
- generar una solicitud de pedido
- iniciar un flujo de aviso o sustitucion
### Flujo funcional propuesto
1. El usuario abre el escaner de TSI desde la app.
2. La app lee la tarjeta y extrae los identificadores necesarios.
3. El backend valida la informacion y la vincula al usuario autenticado.
4. Se obtiene la lista de medicamentos asociados o derivados del contexto del usuario.
5. El sistema cruza esa lista con:
- farmacias cercanas
- stock disponible
- precio
- horario
6. Si no existe stock suficiente, la app muestra alternativas y acciones posibles.
### Consideraciones tecnicas
- Esta funcionalidad requiere definir el formato exacto de lectura de la TSI y las fuentes de datos sanitarias a consultar.
- Debe tratarse como una funcionalidad sensible, con control de acceso estricto y minimizacion de datos.
- El escaneo deberia integrarse con el mismo modelo de ubicacion, autenticacion y notificaciones que ya existe.
- La logica de contraste stock/medicamento encaja bien con el modelo actual de `pharmacy_medicines`, pero probablemente exigiria ampliar la capa de negocio para soportar reservas, pedidos o estados intermedios.
### Riesgos y dependencias
- Dependencia de la integracion real con sistemas sanitarios externos.
- Posible necesidad de cumplimiento legal adicional por el tipo de dato tratado.
- Definicion de si el sistema solo informa de disponibilidad o si tambien permite operar pedidos/reservas.
- Necesidad de auditar trazabilidad, consentimiento y control de uso de datos.
### Resultado esperado
Esta ampliacion convertiria FarmaFinder de un buscador de disponibilidad a un asistente operativo de acceso al medicamento, capaz de ayudar al usuario a pasar de la consulta del stock a la accion cuando no exista disponibilidad inmediata.
@@ -0,0 +1,95 @@
%PDF-1.4
1 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>
endobj
2 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Length 645 >>
stream
BT /F2 18 Tf 126 783.89 Td (DOCUMENTO DE PRECONVERSION) Tj ET
BT /F1 12 Tf 111 754.85 Td (Oficina Española de Patentes y Marcas \(OEPM\)) Tj ET
BT /F3 12 Tf 136 723.49 Td (Hoja de preparacion para expediente de patente) Tj ET
0.8 w 56 630.13 483.28 66 re S
BT /F1 11 Tf 66 696.13 Td (Solicitante: Antoni Nuñez Romeu) Tj ET
BT /F1 11 Tf 66 682.05 Td (DNI/NIF: 45858029P) Tj ET
BT /F1 11 Tf 66 667.9699999999999 Td (Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221) Tj ET
BT /F1 11 Tf 66 653.8899999999999 Td (Inventor: Antoni Nuñez Romeu) Tj ET
BT /F3 10 Tf 126 631.8099999999998 Td (Documento de apoyo para expediente de patente) Tj ET
endstream
endobj
5 0 obj
<< /Length 3202 >>
stream
BT /F2 13 Tf 56 783.89 Td (1. Identificacion del expediente) Tj ET
0.2 w 56 763.25 m 539.28 763.25 l S
BT /F1 11.2 Tf 56 753.25 Td (Solicitante: Antoni Nuñez Romeu) Tj ET
BT /F1 11.2 Tf 56 737.914 Td (NIF/NIE/DNI: 45858029P) Tj ET
BT /F1 11.2 Tf 56 722.578 Td (Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221) Tj ET
BT /F1 11.2 Tf 56 707.242 Td (Titulo de la invencion: Sistema y procedimiento implementados por ordenador para) Tj ET
BT /F1 11.2 Tf 56 692.906 Td (la identificacion de necesidades terapeuticas mediante escaneo de tarjeta) Tj ET
BT /F1 11.2 Tf 56 678.5699999999999 Td (sanitaria individual, contraste con stock farmacutico geolocalizado y generacion) Tj ET
BT /F1 11.2 Tf 56 664.2339999999999 Td (de acciones de suministro.) Tj ET
BT /F2 13 Tf 56 648.8979999999999 Td (2. Objeto de este documento) Tj ET
0.2 w 56 628.2579999999999 m 539.28 628.2579999999999 l S
BT /F1 11.2 Tf 56 618.2579999999999 Td (Este documento se emite como hoja interna de preparacion y control del expediente) Tj ET
BT /F1 11.2 Tf 56 603.9219999999999 Td (antes de su presentacion ante la Oficina Española de Patentes y Marcas \(OEPM\). Su) Tj ET
BT /F1 11.2 Tf 56 589.5859999999999 Td (finalidad es reunir en un unico soporte los datos de identificacion, la referencia) Tj ET
BT /F1 11.2 Tf 56 575.2499999999999 Td (del objeto tecnico y la declaracion de que la documentacion principal del) Tj ET
BT /F1 11.2 Tf 56 560.9139999999999 Td (expediente ha sido preparada para su presentacion.) Tj ET
BT /F2 13 Tf 56 545.5779999999999 Td (3. Contenido del expediente) Tj ET
0.2 w 56 524.9379999999999 m 539.28 524.9379999999999 l S
BT /F1 11.1 Tf 64 514.9379999999999 Td (" Solicitud de patente.) Tj ET
BT /F1 11.1 Tf 64 500.72999999999985 Td (" Memoria descriptiva.) Tj ET
BT /F1 11.1 Tf 64 486.5219999999998 Td (" Reivindicaciones.) Tj ET
BT /F1 11.1 Tf 64 472.3139999999998 Td (" Resumen.) Tj ET
BT /F1 11.1 Tf 64 458.10599999999977 Td (" Anexo de dibujos.) Tj ET
BT /F1 11.1 Tf 64 443.89799999999974 Td (" Solicitud de reduccion de tasas para persona fisica.) Tj ET
BT /F2 13 Tf 56 429.6899999999997 Td (4. Declaracion) Tj ET
0.2 w 56 409.0499999999997 m 539.28 409.0499999999997 l S
BT /F1 11.2 Tf 56 399.0499999999997 Td (El solicitante declara que la documentacion aportada se corresponde con una unica) Tj ET
BT /F1 11.2 Tf 56 384.7139999999997 Td (invencion, que la descripcion y las reivindicaciones han sido redactadas de forma) Tj ET
BT /F1 11.2 Tf 56 370.3779999999997 Td (coherente y que el material adjunto se presenta a efectos de tramitacion ante la) Tj ET
BT /F1 11.2 Tf 56 356.0419999999997 Td (OEPM.) Tj ET
BT /F2 13 Tf 56 340.7059999999997 Td (5. Firma) Tj ET
0.2 w 56 320.0659999999997 m 539.28 320.0659999999997 l S
BT /F1 11.2 Tf 56 310.0659999999997 Td (En Terrassa, a 22 de junio de 2026.) Tj ET
BT /F1 11.2 Tf 56 294.7299999999997 Td (Fdo.: Antoni Nuñez Romeu) Tj ET
BT /F1 11.2 Tf 56 279.39399999999966 Td (DNI/NIF: 45858029P) Tj ET
BT /F1 11.2 Tf 56 264.05799999999965 Td (Documento de apoyo interno. Revisar con agente de propiedad industrial antes de) Tj ET
BT /F1 11.2 Tf 56 249.72199999999964 Td (presentar.) Tj ET
BT /F1 9 Tf 495.28 234.38599999999963 Td (Pagina 2) Tj ET
endstream
endobj
6 0 obj
<< /Type /Pages /Kids [7 0 R 8 0 R] /Count 2 >>
endobj
7 0 obj
<< /Type /Page /Parent 6 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 4 0 R >>
endobj
8 0 obj
<< /Type /Page /Parent 6 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 5 0 R >>
endobj
9 0 obj
<< /Type /Catalog /Pages 6 0 R >>
endobj
xref
0 10
0000000000 65535 f
0000000009 00000 n
0000000108 00000 n
0000000206 00000 n
0000000306 00000 n
0000001002 00000 n
0000004256 00000 n
0000004319 00000 n
0000004471 00000 n
0000004623 00000 n
trailer << /Size 10 /Root 9 0 R >>
startxref
4672
%%EOF
@@ -0,0 +1,92 @@
%PDF-1.4
1 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>
endobj
2 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Length 618 >>
stream
BT /F2 18 Tf 126 783.89 Td (SOLICITUD DE REDUCCION DE TASAS) Tj ET
BT /F1 12 Tf 111 754.85 Td (Oficina Española de Patentes y Marcas \(OEPM\)) Tj ET
BT /F3 12 Tf 136 723.49 Td (Persona fisica) Tj ET
0.8 w 56 630.13 483.28 66 re S
BT /F1 11 Tf 66 696.13 Td (Solicitante: Antoni Nuñez Romeu) Tj ET
BT /F1 11 Tf 66 682.05 Td (DNI/NIF: 45858029P) Tj ET
BT /F1 11 Tf 66 667.9699999999999 Td (Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221) Tj ET
BT /F1 11 Tf 66 653.8899999999999 Td (Inventor: Antoni Nuñez Romeu) Tj ET
BT /F3 10 Tf 126 631.8099999999998 Td (Documento de apoyo para expediente de patente) Tj ET
endstream
endobj
5 0 obj
<< /Length 3068 >>
stream
BT /F2 13 Tf 56 783.89 Td (1. Datos del solicitante) Tj ET
0.2 w 56 763.25 m 539.28 763.25 l S
BT /F1 11.2 Tf 56 753.25 Td (Nombre y apellidos: Antoni Nuñez Romeu) Tj ET
BT /F1 11.2 Tf 56 737.914 Td (DNI/NIF: 45858029P) Tj ET
BT /F1 11.2 Tf 56 722.578 Td (Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221) Tj ET
BT /F2 13 Tf 56 707.242 Td (2. Expediente al que se refiere) Tj ET
0.2 w 56 686.602 m 539.28 686.602 l S
BT /F1 11.2 Tf 56 676.602 Td (Titulo de la invencion: Sistema y procedimiento implementados por ordenador para) Tj ET
BT /F1 11.2 Tf 56 662.266 Td (la identificacion de necesidades terapeuticas mediante escaneo de tarjeta) Tj ET
BT /F1 11.2 Tf 56 647.93 Td (sanitaria individual, contraste con stock farmacutico geolocalizado y generacion) Tj ET
BT /F1 11.2 Tf 56 633.5939999999999 Td (de acciones de suministro.) Tj ET
BT /F2 13 Tf 56 618.2579999999999 Td (3. Solicitud) Tj ET
0.2 w 56 597.6179999999999 m 539.28 597.6179999999999 l S
BT /F1 11.2 Tf 56 587.6179999999999 Td (Por medio del presente escrito, el solicitante, en su condicion de persona fisica,) Tj ET
BT /F1 11.2 Tf 56 573.2819999999999 Td (solicita que se le aplique la reduccion de tasas que resulte procedente conforme a) Tj ET
BT /F1 11.2 Tf 56 558.9459999999999 Td (la normativa vigente de la Oficina Española de Patentes y Marcas \(OEPM\) para) Tj ET
BT /F1 11.2 Tf 56 544.6099999999999 Td (personas fisicas.) Tj ET
BT /F1 11.2 Tf 56 529.2739999999999 Td (A tal efecto, manifiesta que la solicitud de patente se presenta a su nombre como) Tj ET
BT /F1 11.2 Tf 56 514.9379999999999 Td (persona fisica y pide que la oficina tramite esta peticion de conformidad con los) Tj ET
BT /F1 11.2 Tf 56 500.60199999999986 Td (requisitos y porcentajes de reduccion establecidos en la normativa aplicable al) Tj ET
BT /F1 11.2 Tf 56 486.26599999999985 Td (momento de la presentacion.) Tj ET
BT /F2 13 Tf 56 470.92999999999984 Td (4. Declaracion responsable) Tj ET
0.2 w 56 450.28999999999985 m 539.28 450.28999999999985 l S
BT /F1 11.2 Tf 56 440.28999999999985 Td (El solicitante declara bajo su responsabilidad que los datos consignados en este) Tj ET
BT /F1 11.2 Tf 56 425.95399999999984 Td (documento son ciertos y que aportara, en su caso, la documentacion adicional que) Tj ET
BT /F1 11.2 Tf 56 411.6179999999998 Td (la OEPM pudiera requerir para acreditar la condicion de persona fisica y la) Tj ET
BT /F1 11.2 Tf 56 397.2819999999998 Td (procedencia de la reduccion solicitada.) Tj ET
BT /F2 13 Tf 56 381.9459999999998 Td (5. Firma) Tj ET
0.2 w 56 361.3059999999998 m 539.28 361.3059999999998 l S
BT /F1 11.2 Tf 56 351.3059999999998 Td (En Terrassa, a 22 de junio de 2026.) Tj ET
BT /F1 11.2 Tf 56 335.9699999999998 Td (Fdo.: Antoni Nuñez Romeu) Tj ET
BT /F1 11.2 Tf 56 320.6339999999998 Td (DNI/NIF: 45858029P) Tj ET
BT /F1 11.2 Tf 56 305.2979999999998 Td (Nota: revisar con agente o con la OEPM el formulario exacto y los requisitos) Tj ET
BT /F1 11.2 Tf 56 290.96199999999976 Td (vigentes antes de presentar.) Tj ET
BT /F1 9 Tf 495.28 275.62599999999975 Td (Pagina 2) Tj ET
endstream
endobj
6 0 obj
<< /Type /Pages /Kids [7 0 R 8 0 R] /Count 2 >>
endobj
7 0 obj
<< /Type /Page /Parent 6 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 4 0 R >>
endobj
8 0 obj
<< /Type /Page /Parent 6 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 5 0 R >>
endobj
9 0 obj
<< /Type /Catalog /Pages 6 0 R >>
endobj
xref
0 10
0000000000 65535 f
0000000009 00000 n
0000000108 00000 n
0000000206 00000 n
0000000306 00000 n
0000000975 00000 n
0000004095 00000 n
0000004158 00000 n
0000004310 00000 n
0000004462 00000 n
trailer << /Size 10 /Root 9 0 R >>
startxref
4511
%%EOF
@@ -0,0 +1,288 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Solicitud de patente OEPM - FarmaFinder</title>
<style>
@page {
size: A4;
margin: 22mm 20mm 22mm 20mm;
}
body {
font-family: "Liberation Serif", Georgia, serif;
font-size: 11.5pt;
line-height: 1.45;
color: #111;
margin: 0;
}
.page {
page-break-after: always;
}
.page:last-child {
page-break-after: auto;
}
h1, h2, h3 {
margin: 0 0 8px 0;
font-weight: 700;
}
h1 {
font-size: 18pt;
text-align: center;
margin-top: 42mm;
letter-spacing: 0.2px;
}
h2 {
font-size: 13.5pt;
margin-top: 18px;
border-bottom: 1px solid #444;
padding-bottom: 3px;
}
h3 {
font-size: 11.8pt;
margin-top: 14px;
}
.center { text-align: center; }
.meta {
margin-top: 22mm;
font-size: 12pt;
text-align: center;
}
.meta p { margin: 6px 0; }
.box {
border: 1px solid #444;
padding: 10px 12px;
margin: 12px 0;
}
.label {
font-weight: 700;
}
ul, ol {
margin: 6px 0 10px 24px;
padding: 0;
}
li { margin: 3px 0; }
.small {
font-size: 10pt;
}
.claims p {
margin: 6px 0 10px 0;
text-align: justify;
}
.claims .claim {
margin-bottom: 12px;
}
.indent {
margin-left: 18px;
}
.spaced p {
margin: 0 0 8px 0;
text-align: justify;
}
.footer-note {
margin-top: 14mm;
font-size: 10pt;
text-align: center;
}
</style>
</head>
<body>
<div class="page">
<h1>SOLICITUD DE PATENTE</h1>
<div class="meta">
<p><span class="label">Oficina:</span> Oficina Española de Patentes y Marcas (OEPM)</p>
<p><span class="label">Título de la invención:</span><br>
Sistema y procedimiento implementados por ordenador para la identificación de necesidades terapéuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacéutico geolocalizado y generación de acciones de suministro.</p>
</div>
<div class="box">
<p><span class="label">Solicitante:</span> Antoni Nuñez Romeu</p>
<p><span class="label">NIF/CIF:</span> 45858029P</p>
<p><span class="label">Domicilio:</span> C/ Font Vella 12, 1, Terrassa, Barcelona, 08221</p>
<p><span class="label">Inventor o inventores:</span> Antoni Nuñez Romeu</p>
</div>
<div class="footer-note">
Memoria descriptiva, reivindicaciones, resumen y referencia a dibujos
</div>
</div>
<div class="page">
<h2>1. Campo técnico de la invención</h2>
<div class="spaced">
<p>La invención se encuadra en los sistemas informáticos aplicados al sector sanitario y farmacéutico, en particular en soluciones de captura de identificadores sanitarios, comparación automatizada de disponibilidad de medicamentos y asistencia digital a la dispensación y suministro.</p>
</div>
<h2>2. Antecedentes de la invención</h2>
<div class="spaced">
<p>Las soluciones actuales de búsqueda de medicamentos y farmacias permiten localizar productos y mostrar stock aproximado, pero normalmente operan a partir de una búsqueda manual por nombre de medicamento o principio activo. También existen sistemas de geolocalización y de gestión de inventario, pero suelen estar desacoplados del contexto sanitario del usuario y no integran, en una única cadena técnica, la identificación mediante tarjeta sanitaria individual, la inferencia de necesidades terapéuticas y la comparación en tiempo real con disponibilidad farmacéutica local.</p>
<p>Además, cuando no existe stock en la farmacia más próxima, el usuario debe iniciar manualmente nuevos procesos de búsqueda o desplazamiento, sin que el sistema proponga de forma automatizada acciones técnicas de reserva, pedido o derivación basadas en la disponibilidad real.</p>
</div>
<h2>3. Problema técnico a resolver</h2>
<div class="spaced">
<p>La invención resuelve el problema técnico de reducir el tiempo y la fricción entre la identificación de una necesidad terapéutica y la localización de una farmacia que disponga del medicamento adecuado, integrando:</p>
<ul>
<li>lectura segura de identificadores sanitarios,</li>
<li>obtención de referencias de medicamento asociadas,</li>
<li>comparación automatizada con inventarios de farmacias,</li>
<li>priorización por proximidad y disponibilidad,</li>
<li>generación de acciones técnicas de suministro cuando no exista stock inmediato.</li>
</ul>
</div>
<h2>4. Resumen de la invención</h2>
<div class="spaced">
<p>La invención propone un sistema y procedimiento implementados por ordenador que operan sobre un terminal móvil o dispositivo de lectura. El sistema captura la información de una tarjeta sanitaria individual mediante escaneo, valida la información mediante un módulo de verificación, asocia el resultado al perfil del usuario autenticado y obtiene una lista de medicamentos o necesidades terapéuticas relevantes. Posteriormente, un motor de contraste consulta un repositorio de stock de farmacias geolocalizadas y determina la coincidencia entre la necesidad detectada y la disponibilidad real en cada establecimiento. Si existe stock, el sistema prioriza las farmacias adecuadas y presenta una ruta de acceso o reserva. Si no existe stock, el sistema genera automáticamente una acción de aviso, pedido, reserva o derivación, conservando trazabilidad técnica del evento. La invención no se limita a una búsqueda manual, sino que integra captura, validación, georreferenciación, consulta de stock y respuesta automática en una única arquitectura técnica.</p>
</div>
</div>
<div class="page">
<h2>5. Breve descripción de los dibujos</h2>
<div class="spaced">
<p><span class="label">Figura 1.</span> Diagrama de bloques general del sistema.</p>
<p><span class="label">Figura 2.</span> Flujo de captura y validación del escaneo de la TSI.</p>
<p><span class="label">Figura 3.</span> Flujo de contraste entre necesidad terapéutica y stock de farmacias.</p>
<p><span class="label">Figura 4.</span> Flujo de acción cuando no existe stock disponible.</p>
</div>
<h2>6. Descripción detallada de la invención</h2>
<h3>6.1 Arquitectura general</h3>
<div class="spaced">
<p>El sistema comprende al menos los siguientes módulos:</p>
<ol>
<li>un módulo de captura de imagen o lectura óptica para escanear la tarjeta sanitaria individual;</li>
<li>un módulo de extracción de datos para identificar los campos relevantes de la tarjeta;</li>
<li>un módulo de verificación de identidad y autorización, vinculado al usuario autenticado;</li>
<li>un módulo de obtención de necesidades terapéuticas o medicamentos asociados;</li>
<li>un módulo de geolocalización para obtener la posición del usuario o una posición de referencia;</li>
<li>un módulo de consulta de inventario para recuperar stock, precio y horario de farmacias;</li>
<li>un módulo de contraste y priorización para ordenar farmacias según disponibilidad y cercanía;</li>
<li>un módulo de generación de acciones para crear reserva, aviso, pedido o derivación;</li>
<li>un módulo de trazabilidad y auditoría.</li>
</ol>
</div>
<h3>6.2 Funcionamiento</h3>
<div class="spaced">
<p>En una realización preferida, el usuario abre la aplicación y solicita el escaneo de su tarjeta sanitaria individual. El terminal captura la imagen o el código de la tarjeta y el módulo de extracción identifica los datos técnicos necesarios para enlazar el contexto sanitario del usuario con un conjunto de medicamentos o referencias de tratamiento.</p>
<p>El sistema valida que la operación se ejecuta sobre un usuario autenticado y autorizado. A continuación, el motor de contraste consulta el inventario de farmacias cercanas, recuperando para cada farmacia al menos su ubicación, horario, stock y, opcionalmente, precio.</p>
<p>El módulo de priorización calcula una orden de resultados en base a criterios técnicos tales como:</p>
<ul>
<li>existencia de stock,</li>
<li>nivel de stock,</li>
<li>distancia geográfica,</li>
<li>horario de apertura,</li>
<li>capacidad de reserva o solicitud de pedido.</li>
</ul>
<p>Cuando existe disponibilidad, el sistema muestra una lista priorizada de farmacias y puede iniciar una reserva o una indicación de recogida. Cuando no existe disponibilidad, el sistema no se limita a informar de la falta de stock, sino que genera una acción técnica adicional, por ejemplo:</p>
<ul>
<li>alerta a la farmacia seleccionada,</li>
<li>solicitud de pedido a distribución o proveedor,</li>
<li>derivación automática a otra farmacia con stock,</li>
<li>registro de una lista de espera o interés de suministro.</li>
</ul>
</div>
<h3>6.3 Tratamiento de datos y seguridad</h3>
<div class="spaced">
<p>La invención prevé que la información derivada del escaneo de la TSI se trate con medidas de minimización de datos, control de acceso, cifrado en tránsito y registro de eventos. El sistema no expone innecesariamente los datos de la tarjeta sanitaria y solo conserva los elementos imprescindibles para la operación descrita.</p>
</div>
<h3>6.4 Integración con la plataforma FarmaFinder</h3>
<div class="spaced">
<p>En una implementación concreta, el sistema puede integrarse con una plataforma que ya disponga de:</p>
<ul>
<li>catálogo de farmacias,</li>
<li>geolocalización,</li>
<li>stock por medicamento,</li>
<li>autenticación de usuario,</li>
<li>notificaciones push,</li>
<li>histórico de acciones y preferencias.</li>
</ul>
<p>La integración permite que el resultado del escaneo de la TSI no se limite a una consulta pasiva, sino que se convierta en un disparador de decisiones técnicas sobre disponibilidad y suministro.</p>
</div>
</div>
<div class="page">
<h2>7. Ejemplo de realización</h2>
<div class="spaced">
<ol>
<li>El usuario inicia sesión en la aplicación.</li>
<li>El usuario escanea su TSI con la cámara del terminal.</li>
<li>El sistema extrae el identificador necesario y lo vincula al perfil autorizado.</li>
<li>El motor terapéutico asocia una necesidad de medicamento o tratamiento.</li>
<li>El sistema consulta farmacias cercanas con stock del medicamento.</li>
<li>Si hay stock, ordena las farmacias por proximidad y disponibilidad.</li>
<li>Si no hay stock, genera una reserva, una alerta o una solicitud de pedido.</li>
</ol>
</div>
<h2>8. Reivindicaciones</h2>
<div class="claims">
<div class="claim">
<p><span class="label">Reivindicación 1.</span> Sistema implementado por ordenador para identificar una necesidad terapéutica a partir del escaneo de una tarjeta sanitaria individual, caracterizado porque comprende medios de captura para obtener datos de la tarjeta sanitaria individual; medios de verificación para asociar dichos datos a un usuario autenticado; medios de obtención de una referencia de medicamento o necesidad terapéutica; medios de consulta de inventario para recuperar stock de farmacias geolocalizadas; medios de contraste para comparar la referencia de medicamento o necesidad terapéutica con el stock disponible; y medios de generación de acciones para producir una reserva, aviso, pedido o derivación cuando el stock disponible sea insuficiente.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 2.</span> Sistema según la reivindicación 1, en el que los medios de consulta de inventario ordenan las farmacias según una combinación de stock disponible, proximidad geográfica y horario de apertura.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 3.</span> Sistema según cualquiera de las reivindicaciones anteriores, en el que los datos de la tarjeta sanitaria individual son tratados mediante un módulo de minimización y cifrado para limitar su almacenamiento a los campos técnicamente necesarios.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 4.</span> Sistema según cualquiera de las reivindicaciones anteriores, en el que la generación de acciones comprende la creación de una reserva, la emisión de una notificación a la farmacia o la solicitud de pedido a un circuito de suministro.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 5.</span> Sistema según cualquiera de las reivindicaciones anteriores, en el que la comparación de stock se realiza contra un repositorio de farmacias enlazadas con medicamentos mediante un identificador de registro farmacológico.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 6.</span> Sistema según cualquiera de las reivindicaciones anteriores, en el que la geolocalización del usuario se obtiene a partir de coordenadas almacenadas en un perfil autenticado o a partir del dispositivo de lectura.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 7.</span> Procedimiento implementado por ordenador para asistir en la localización y suministro de medicamentos, caracterizado porque comprende escanear una tarjeta sanitaria individual; verificar la autorización del usuario; derivar una necesidad terapéutica o referencia de medicamento; consultar stock de farmacias geolocalizadas; comparar la necesidad con el stock disponible; y generar una acción técnica de reserva, aviso, pedido o derivación cuando no exista disponibilidad suficiente.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 8.</span> Procedimiento según la reivindicación 7, en el que la comparación y priorización se ejecutan en tiempo real o cuasi tiempo real durante la interacción del usuario con la aplicación.</p>
</div>
<div class="claim">
<p><span class="label">Reivindicación 9.</span> Dispositivo móvil, terminal o sistema de lectura que comprende una cámara o lector óptico y que está configurado para ejecutar el sistema de cualquiera de las reivindicaciones 1 a 6.</p>
</div>
</div>
</div>
<div class="page">
<h2>9. Aplicación industrial</h2>
<div class="spaced">
<p>La invención es susceptible de aplicación industrial en plataformas de asistencia farmacéutica, sistemas de gestión de inventario, servicios de reserva o pedido de medicamentos y terminales de consulta sanitaria.</p>
</div>
<h2>10. Resumen</h2>
<div class="spaced">
<p>La presente invención se refiere a un sistema y procedimiento implementados por ordenador que permiten, a partir del escaneo de una tarjeta sanitaria individual mediante un terminal móvil o dispositivo equivalente, asociar de forma segura un contexto de paciente a una necesidad terapéutica, obtener una referencia de medicamentos relevantes, comparar dicha necesidad con el stock disponible en farmacias geolocalizadas y generar, en caso de falta de disponibilidad, una acción de reserva, aviso, solicitud de pedido o derivación a una farmacia alternativa. El sistema integra captura de datos, verificación de identidad, geolocalización, consulta de disponibilidad, gestión de stock y generación de eventos de suministro en una única cadena técnica de procesamiento.</p>
</div>
<h2>11. Notas para presentación en OEPM</h2>
<h2>11. Anexo de dibujos</h2>
<div class="spaced">
<p>Las figuras indicadas en la presente memoria tienen carácter esquemático y se incorporan a efectos ilustrativos de la arquitectura, del flujo de captura, del contraste de stock y de la generación de acciones. En una presentación definitiva, dichas figuras podrán acompañarse como láminas separadas numeradas de forma consecutiva.</p>
</div>
<h2>12. Firma del solicitante</h2>
<div class="spaced">
<p>En Terrassa, a 22 de junio de 2026.</p>
<p>Fdo.: Antoni Nuñez Romeu</p>
<p>DNI/NIF: 45858029P</p>
</div>
<h2>13. Notas para presentación en OEPM</h2>
<div class="spaced small">
<ul>
<li>La solicitud debe presentarse como una única invención o un único concepto inventivo general.</li>
<li>La descripción, reivindicaciones y dibujos deben ser coherentes entre sí.</li>
<li>La protección en patente debe apoyarse en un efecto técnico concreto y no en una mera regla comercial o método abstracto.</li>
<li>Esta redacción evita formular una reivindicación independiente de "programa de ordenador como tal" y se centra en un sistema y un procedimiento implementados por ordenador con interacción técnica sobre captura, contraste de inventario y generación de acciones.</li>
<li>Antes de presentar la solicitud conviene una búsqueda de anterioridades y una revisión por agente de propiedad industrial.</li>
</ul>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
# Solicitud de patente
## Titulo de la invencion
Sistema y procedimiento implementados por ordenador para la identificacion de necesidades terapeuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacutico geolocalizado y generacion de acciones de suministro.
## Solicitante
- Nombre / razon social: Antoni Nuñez Romeu
- NIF/CIF: 45858029P
- Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221
## Inventor o inventores
- Nombre y apellidos: Antoni Nuñez Romeu
- Direccion de contacto: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221
## Resumen
La invencion se refiere a un sistema y un procedimiento implementados por ordenador que permiten, a partir del escaneo de una tarjeta sanitaria individual mediante un terminal movil o dispositivo equivalente, asociar de forma segura un contexto de paciente a una necesidad terapeutica, obtener una referencia de medicamentos relevantes, comparar dicha necesidad con el stock disponible en farmacias geolocalizadas y generar, en caso de falta de disponibilidad, una accion de reserva, aviso, solicitud de pedido o derivacion a una farmacia alternativa. El sistema integra captura de datos, verificacion de identidad, geolocalizacion, consulta de disponibilidad, gestion de stock y generacion de eventos de suministro en una unica cadena tecnica de procesamiento.
## Campo tecnico de la invencion
La invencion se encuadra en los sistemas informaticos aplicados al sector sanitario y farmaceutico, en particular en soluciones de captura de identificadores sanitarios, comparacion automatizada de disponibilidad de medicamentos y asistencia digital a la dispensacion y suministro.
## Antecedentes de la invencion
Las soluciones actuales de busqueda de medicamentos y farmacias permiten localizar productos y mostrar stock aproximado, pero normalmente operan a partir de una busqueda manual por nombre de medicamento o principio activo. Tambien existen sistemas de geolocalizacion y de gestion de inventario, pero suelen estar desacoplados del contexto sanitario del usuario y no integran, en una unica cadena tecnica, la identificacion mediante tarjeta sanitaria individual, la inferencia de necesidades terapeuticas y la comparacion en tiempo real con disponibilidad farmaceutica local.
Ademas, cuando no existe stock en la farmacia mas proxima, el usuario debe iniciar manualmente nuevos procesos de busqueda o desplazamiento, sin que el sistema proponga de forma automatizada acciones tecnicas de reserva, pedido o derivacion basadas en la disponibilidad real.
## Problema tecnico a resolver
La invencion resuelve el problema tecnico de reducir el tiempo y la friccion entre la identificacion de una necesidad terapeutica y la localizacion de una farmacia que disponga del medicamento adecuado, integrando:
- lectura segura de identificadores sanitarios,
- obtencion de referencias de medicamento asociadas,
- comparacion automatizada con inventarios de farmacias,
- priorizacion por proximidad y disponibilidad,
- generacion de acciones tecnicas de suministro cuando no exista stock inmediato.
## Resumen de la invencion
La invencion propone un sistema y procedimiento implementados por ordenador que operan sobre un terminal movil o dispositivo de lectura. El sistema captura la informacion de una tarjeta sanitaria individual mediante escaneo, valida la informacion mediante un modulo de verificacion, asocia el resultado al perfil del usuario autenticado y obtiene una lista de medicamentos o necesidades terapeuticas relevantes. Posteriormente, un motor de contraste consulta un repositorio de stock de farmacias geolocalizadas y determina la coincidencia entre la necesidad detectada y la disponibilidad real en cada establecimiento. Si existe stock, el sistema prioriza las farmacias adecuadas y presenta una ruta de acceso o reserva. Si no existe stock, el sistema genera automaticamente una accion de aviso, pedido, reserva o derivacion, conservando trazabilidad tecnica del evento. La invencion no se limita a una busqueda manual, sino que integra captura, validacion, georreferenciacion, consulta de stock y respuesta automatica en una unica arquitectura tecnica.
## Breve descripcion de los dibujos
**Figura 1.** Diagrama de bloques general del sistema.
**Figura 2.** Flujo de captura y validacion del escaneo de la TSI.
**Figura 3.** Flujo de contraste entre necesidad terapeutica y stock de farmacias.
**Figura 4.** Flujo de accion cuando no existe stock disponible.
## Descripcion detallada de la invencion
### 1. Arquitectura general
El sistema comprende al menos los siguientes modulos:
1. Un modulo de captura de imagen o lectura optica para escanear la tarjeta sanitaria individual.
2. Un modulo de extraccion de datos para identificar los campos relevantes de la tarjeta.
3. Un modulo de verificacion de identidad y autorizacion, vinculado al usuario autenticado.
4. Un modulo de obtencion de necesidades terapeuticas o medicamentos asociados.
5. Un modulo de geolocalizacion para obtener la posicion del usuario o una posicion de referencia.
6. Un modulo de consulta de inventario para recuperar stock, precio y horario de farmacias.
7. Un modulo de contraste y priorizacion para ordenar farmacias segun disponibilidad y cercania.
8. Un modulo de generacion de acciones para crear reserva, aviso, pedido o derivacion.
9. Un modulo de trazabilidad y auditoria.
### 2. Funcionamiento
En una realizacion preferida, el usuario abre la aplicacion y solicita el escaneo de su tarjeta sanitaria individual. El terminal captura la imagen o el codigo de la tarjeta y el modulo de extraccion identifica los datos tecnicos necesarios para enlazar el contexto sanitario del usuario con un conjunto de medicamentos o referencias de tratamiento.
El sistema valida que la operacion se ejecuta sobre un usuario autenticado y autorizado. A continuacion, el motor de contraste consulta el inventario de farmacias cercanas, recuperando para cada farmacia al menos su ubicacion, horario, stock y, opcionalmente, precio.
El modulo de priorizacion calcula una orden de resultados en base a criterios tecnicos tales como:
- existencia de stock,
- nivel de stock,
- distancia geografica,
- horario de apertura,
- capacidad de reserva o solicitud de pedido.
Cuando existe disponibilidad, el sistema muestra una lista priorizada de farmacias y puede iniciar una reserva o una indicacion de recogida. Cuando no existe disponibilidad, el sistema no se limita a informar de la falta de stock, sino que genera una accion tecnica adicional, por ejemplo:
- alerta a la farmacia seleccionada,
- solicitud de pedido a distribucion o proveedor,
- derivacion automatica a otra farmacia con stock,
- registro de una lista de espera o interes de suministro.
### 3. Tratamiento de datos y seguridad
La invencion prevé que la informacion derivada del escaneo de la TSI se trate con medidas de minimizacion de datos, control de acceso, cifrado en transito y registro de eventos. El sistema no expone innecesariamente los datos de la tarjeta sanitaria y solo conserva los elementos imprescindibles para la operacion descrita.
### 4. Integracion con la plataforma FarmaFinder
En una implementacion concreta, el sistema puede integrarse con una plataforma que ya disponga de:
- catalogo de farmacias,
- geolocalizacion,
- stock por medicamento,
- autenticacion de usuario,
- notificaciones push,
- historico de acciones y preferencias.
La integracion permite que el resultado del escaneo de la TSI no se limite a una consulta pasiva, sino que se convierta en un disparador de decisiones tecnicas sobre disponibilidad y suministro.
## Ejemplo de realizacion
1. El usuario inicia sesion en la aplicacion.
2. El usuario escanea su TSI con la camara del terminal.
3. El sistema extrae el identificador necesario y lo vincula al perfil autorizado.
4. El motor terapeutico asocia una necesidad de medicamento o tratamiento.
5. El sistema consulta farmacias cercanas con stock del medicamento.
6. Si hay stock, ordena las farmacias por proximidad y disponibilidad.
7. Si no hay stock, genera una reserva, una alerta o una solicitud de pedido.
## Reivindicaciones
### Reivindicacion 1
Sistema implementado por ordenador para identificar una necesidad terapeutica a partir del escaneo de una tarjeta sanitaria individual, caracterizado porque comprende:
- medios de captura para obtener datos de la tarjeta sanitaria individual;
- medios de verificacion para asociar dichos datos a un usuario autenticado;
- medios de obtencion de una referencia de medicamento o necesidad terapeutica;
- medios de consulta de inventario para recuperar stock de farmacias geolocalizadas;
- medios de contraste para comparar la referencia de medicamento o necesidad terapeutica con el stock disponible; y
- medios de generacion de acciones para producir una reserva, aviso, pedido o derivacion cuando el stock disponible sea insuficiente.
### Reivindicacion 2
Sistema segun la reivindicacion 1, en el que los medios de consulta de inventario ordenan las farmacias segun una combinacion de stock disponible, proximidad geografica y horario de apertura.
### Reivindicacion 3
Sistema segun cualquiera de las reivindicaciones anteriores, en el que los datos de la tarjeta sanitaria individual son tratados mediante un modulo de minimizacion y cifrado para limitar su almacenamiento a los campos tecnicamente necesarios.
### Reivindicacion 4
Procedimiento implementado por ordenador para asistir en la localizacion y suministro de medicamentos, caracterizado porque comprende:
1. escanear una tarjeta sanitaria individual;
2. verificar la autorizacion del usuario;
3. derivar una necesidad terapeutica o referencia de medicamento;
4. consultar stock de farmacias geolocalizadas;
5. comparar la necesidad con el stock disponible; y
6. generar una accion tecnica de reserva, aviso, pedido o derivacion cuando no exista disponibilidad suficiente.
### Reivindicacion 5
Dispositivo movil, terminal o sistema de lectura que comprende una camara o lector optico y que esta configurado para ejecutar el sistema de cualquiera de las reivindicaciones anteriores.
## Aplicacion industrial
La invencion es susceptible de aplicacion industrial en plataformas de asistencia farmaceutica, sistemas de gestion de inventario, servicios de reserva o pedido de medicamentos y terminales de consulta sanitaria.
## Notas para presentacion en OEPM
- La solicitud debe presentarse como una unica invencion o un unico concepto inventivo general.
- La descripcion, reivindicaciones y dibujos deben ser coherentes entre si.
- La proteccion en patente debe apoyarse en un efecto tecnico concreto y no en una mera regla comercial o metodo abstracto.
- Esta redaccion evita formular una reivindicacion independiente de "programa de ordenador como tal" y se centra en un sistema y un procedimiento implementados por ordenador con interaccion tecnica sobre captura, contraste de inventario y generacion de acciones.
- Las figuras deben presentarse como laminas separadas si se aportan al expediente.
- Conviene incluir firma del solicitante y fecha de presentacion en el juego documental final.
- Antes de presentar la solicitud conviene una busqueda de anterioridades y una revision por agente de propiedad industrial.
## Firma
En Terrassa, a 22 de junio de 2026.
Fdo.: Antoni Nuñez Romeu
DNI/NIF: 45858029P
@@ -0,0 +1,290 @@
%PDF-1.4
1 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>
endobj
2 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Length 1104 >>
stream
BT /F2 18 Tf 141 783.89 Td (SOLICITUD DE PATENTE) Tj ET
BT /F1 12 Tf 111 754.85 Td (Oficina Española de Patentes y Marcas \(OEPM\)) Tj ET
BT /F2 12 Tf 176 725.49 Td (Título de la invención:) Tj ET
BT /F1 12 Tf 66 710.13 Td (Sistema y procedimiento implementados por ordenador para la identificación de) Tj ET
BT /F1 12 Tf 66 694.77 Td (necesidades terapéuticas mediante escaneo de tarjeta sanitaria individual,) Tj ET
BT /F1 12 Tf 66 679.41 Td (contraste con stock farmacéutico geolocalizado y generación de acciones de) Tj ET
BT /F1 12 Tf 66 664.05 Td (suministro.) Tj ET
0.8 w 56 580.6899999999999 483.28 68 re S
BT /F1 11 Tf 66 636.6899999999999 Td (Solicitante: Antoni Nuñez Romeu) Tj ET
BT /F1 11 Tf 66 622.6099999999999 Td (NIF/CIF: 45858029P) Tj ET
BT /F1 11 Tf 66 608.5299999999999 Td (Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221) Tj ET
BT /F1 11 Tf 66 594.4499999999998 Td (Inventor o inventores: Antoni Nuñez Romeu) Tj ET
BT /F3 10 Tf 124 568.3699999999998 Td (Memoria descriptiva, reivindicaciones, resumen y referencia a dibujos) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 1) Tj ET
endstream
endobj
5 0 obj
<< /Length 4950 >>
stream
BT /F2 13 Tf 56 781.89 Td (1. Campo técnico de la invención) Tj ET
0.2 w 56 761.25 m 539.28 761.25 l S
BT /F1 11.5 Tf 56 751.25 Td (La invención se encuadra en los sistemas informáticos aplicados al sector sanitario) Tj ET
BT /F1 11.5 Tf 56 736.53 Td (y farmacéutico, en particular en soluciones de captura de identificadores) Tj ET
BT /F1 11.5 Tf 56 721.81 Td (sanitarios, comparación automatizada de disponibilidad de medicamentos y asistencia) Tj ET
BT /F1 11.5 Tf 56 707.0899999999999 Td (digital a la dispensación y suministro.) Tj ET
BT /F2 13 Tf 56 688.3699999999999 Td (2. Antecedentes de la invención) Tj ET
0.2 w 56 667.7299999999999 m 539.28 667.7299999999999 l S
BT /F1 11.5 Tf 56 657.7299999999999 Td (Las soluciones actuales de búsqueda de medicamentos y farmacias permiten localizar) Tj ET
BT /F1 11.5 Tf 56 643.0099999999999 Td (productos y mostrar stock aproximado, pero normalmente operan a partir de una) Tj ET
BT /F1 11.5 Tf 56 628.2899999999998 Td (búsqueda manual por nombre de medicamento o principio activo. También existen) Tj ET
BT /F1 11.5 Tf 56 613.5699999999998 Td (sistemas de geolocalización y de gestión de inventario, pero suelen estar) Tj ET
BT /F1 11.5 Tf 56 598.8499999999998 Td (desacoplados del contexto sanitario del usuario y no integran, en una única cadena) Tj ET
BT /F1 11.5 Tf 56 584.1299999999998 Td (técnica, la identificación mediante tarjeta sanitaria individual, la inferencia de) Tj ET
BT /F1 11.5 Tf 56 569.4099999999997 Td (necesidades terapéuticas y la comparación en tiempo real con disponibilidad) Tj ET
BT /F1 11.5 Tf 56 554.6899999999997 Td (farmacéutica local.) Tj ET
BT /F1 11.5 Tf 56 537.9699999999997 Td (Además, cuando no existe stock en la farmacia más próxima, el usuario debe iniciar) Tj ET
BT /F1 11.5 Tf 56 523.2499999999997 Td (manualmente nuevos procesos de búsqueda o desplazamiento, sin que el sistema) Tj ET
BT /F1 11.5 Tf 56 508.52999999999963 Td (proponga de forma automatizada acciones técnicas de reserva, pedido o derivación) Tj ET
BT /F1 11.5 Tf 56 493.8099999999996 Td (basadas en la disponibilidad real.) Tj ET
BT /F2 13 Tf 56 475.0899999999996 Td (3. Problema técnico a resolver) Tj ET
0.2 w 56 454.4499999999996 m 539.28 454.4499999999996 l S
BT /F1 11.5 Tf 56 444.4499999999996 Td (La invención resuelve el problema técnico de reducir el tiempo y la fricción entre) Tj ET
BT /F1 11.5 Tf 56 429.72999999999956 Td (la identificación de una necesidad terapéutica y la localización de una farmacia que) Tj ET
BT /F1 11.5 Tf 56 415.00999999999954 Td (disponga del medicamento adecuado, integrando:) Tj ET
BT /F1 11.2 Tf 64 398.2899999999995 Td (" lectura segura de identificadores sanitarios,) Tj ET
BT /F1 11.2 Tf 64 383.9539999999995 Td (" obtención de referencias de medicamento asociadas,) Tj ET
BT /F1 11.2 Tf 64 369.6179999999995 Td (" comparación automatizada con inventarios de farmacias,) Tj ET
BT /F1 11.2 Tf 64 355.28199999999947 Td (" priorización por proximidad y disponibilidad,) Tj ET
BT /F1 11.2 Tf 64 340.94599999999946 Td (" generación de acciones técnicas de suministro cuando no exista stock inmediato.) Tj ET
BT /F2 13 Tf 56 324.60999999999945 Td (4. Resumen de la invención) Tj ET
0.2 w 56 303.96999999999946 m 539.28 303.96999999999946 l S
BT /F1 11.5 Tf 56 293.96999999999946 Td (La invención propone un sistema y procedimiento implementados por ordenador que) Tj ET
BT /F1 11.5 Tf 56 279.24999999999943 Td (operan sobre un terminal móvil o dispositivo de lectura. El sistema captura la) Tj ET
BT /F1 11.5 Tf 56 264.5299999999994 Td (información de una tarjeta sanitaria individual mediante escaneo, valida la) Tj ET
BT /F1 11.5 Tf 56 249.8099999999994 Td (información mediante un módulo de verificación, asocia el resultado al perfil del) Tj ET
BT /F1 11.5 Tf 56 235.0899999999994 Td (usuario autenticado y obtiene una lista de medicamentos o necesidades terapéuticas) Tj ET
BT /F1 11.5 Tf 56 220.3699999999994 Td (relevantes. Posteriormente, un motor de contraste consulta un repositorio de stock) Tj ET
BT /F1 11.5 Tf 56 205.6499999999994 Td (de farmacias geolocalizadas y determina la coincidencia entre la necesidad detectada) Tj ET
BT /F1 11.5 Tf 56 190.9299999999994 Td (y la disponibilidad real en cada establecimiento. Si existe stock, el sistema) Tj ET
BT /F1 11.5 Tf 56 176.2099999999994 Td (prioriza las farmacias adecuadas y presenta una ruta de acceso o reserva. Si no) Tj ET
BT /F1 11.5 Tf 56 161.4899999999994 Td (existe stock, el sistema genera automáticamente una acción de aviso, pedido, reserva) Tj ET
BT /F1 11.5 Tf 56 146.7699999999994 Td (o derivación, conservando trazabilidad técnica del evento. La invención no se limita) Tj ET
BT /F1 11.5 Tf 56 132.04999999999941 Td (a una búsqueda manual, sino que integra captura, validación, georreferenciación,) Tj ET
BT /F1 11.5 Tf 56 117.32999999999942 Td (consulta de stock y respuesta automática en una única arquitectura técnica.) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 2) Tj ET
endstream
endobj
6 0 obj
<< /Length 3436 >>
stream
BT /F2 13 Tf 56 781.89 Td (5. Breve descripción de los dibujos) Tj ET
0.2 w 56 761.25 m 539.28 761.25 l S
BT /F1 11.5 Tf 56 751.25 Td (Figura 1. Diagrama de bloques general del sistema.) Tj ET
BT /F1 11.5 Tf 56 734.53 Td (Figura 2. Flujo de captura y validación del escaneo de la TSI.) Tj ET
BT /F1 11.5 Tf 56 717.81 Td (Figura 3. Flujo de contraste entre necesidad terapéutica y stock de farmacias.) Tj ET
BT /F1 11.5 Tf 56 701.0899999999999 Td (Figura 4. Flujo de acción cuando no existe stock disponible.) Tj ET
BT /F2 13 Tf 56 682.3699999999999 Td (6. Descripción detallada de la invención) Tj ET
0.2 w 56 661.7299999999999 m 539.28 661.7299999999999 l S
BT /F2 11.5 Tf 56 651.7299999999999 Td (6.1 Arquitectura general) Tj ET
BT /F1 11.5 Tf 56 637.0099999999999 Td (El sistema comprende al menos los siguientes módulos:) Tj ET
BT /F1 11.2 Tf 64 620.2899999999998 Td (1. un módulo de captura de imagen o lectura óptica para escanear la tarjeta sanitaria) Tj ET
BT /F1 11.2 Tf 64 605.9539999999998 Td ( individual;) Tj ET
BT /F1 11.2 Tf 64 591.6179999999998 Td (2. un módulo de extracción de datos para identificar los campos relevantes de la tarjeta;) Tj ET
BT /F1 11.2 Tf 64 577.2819999999998 Td (3. un módulo de verificación de identidad y autorización, vinculado al usuario) Tj ET
BT /F1 11.2 Tf 64 562.9459999999998 Td ( autenticado;) Tj ET
BT /F1 11.2 Tf 64 548.6099999999998 Td (4. un módulo de obtención de necesidades terapéuticas o medicamentos asociados;) Tj ET
BT /F1 11.2 Tf 64 534.2739999999998 Td (5. un módulo de geolocalización para obtener la posición del usuario o una posición de) Tj ET
BT /F1 11.2 Tf 64 519.9379999999998 Td ( referencia;) Tj ET
BT /F1 11.2 Tf 64 505.60199999999975 Td (6. un módulo de consulta de inventario para recuperar stock, precio y horario de) Tj ET
BT /F1 11.2 Tf 64 491.26599999999974 Td ( farmacias;) Tj ET
BT /F1 11.2 Tf 64 476.9299999999997 Td (7. un módulo de contraste y priorización para ordenar farmacias según disponibilidad y) Tj ET
BT /F1 11.2 Tf 64 462.5939999999997 Td ( cercanía;) Tj ET
BT /F1 11.2 Tf 64 448.2579999999997 Td (8. un módulo de generación de acciones para crear reserva, aviso, pedido o derivación;) Tj ET
BT /F1 11.2 Tf 64 433.9219999999997 Td (9. un módulo de trazabilidad y auditoría.) Tj ET
BT /F2 11.5 Tf 56 419.5859999999997 Td (6.2 Funcionamiento) Tj ET
BT /F1 11.5 Tf 56 404.86599999999964 Td (En una realización preferida, el usuario abre la aplicación y solicita el escaneo de) Tj ET
BT /F1 11.5 Tf 56 390.1459999999996 Td (su tarjeta sanitaria individual. El terminal captura la imagen o el código de la) Tj ET
BT /F1 11.5 Tf 56 375.4259999999996 Td (tarjeta y el módulo de extracción identifica los datos técnicos necesarios para) Tj ET
BT /F1 11.5 Tf 56 360.70599999999956 Td (enlazar el contexto sanitario del usuario con un conjunto de medicamentos o) Tj ET
BT /F1 11.5 Tf 56 345.98599999999954 Td (referencias de tratamiento.) Tj ET
BT /F1 11.5 Tf 56 329.2659999999995 Td (El sistema valida que la operación se ejecuta sobre un usuario autenticado y) Tj ET
BT /F1 11.5 Tf 56 314.5459999999995 Td (autorizado. A continuación, el motor de contraste consulta el inventario de) Tj ET
BT /F1 11.5 Tf 56 299.82599999999945 Td (farmacias cercanas, recuperando para cada farmacia al menos su ubicación, horario,) Tj ET
BT /F1 11.5 Tf 56 285.1059999999994 Td (stock y, opcionalmente, precio.) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 3) Tj ET
endstream
endobj
7 0 obj
<< /Length 3271 >>
stream
BT /F2 11.5 Tf 56 783.89 Td (6.2 Funcionamiento \(continuación\)) Tj ET
BT /F1 11.5 Tf 56 769.17 Td (El módulo de priorización calcula una orden de resultados en base a criterios) Tj ET
BT /F1 11.5 Tf 56 754.4499999999999 Td (técnicos tales como existencia de stock, nivel de stock, distancia geográfica,) Tj ET
BT /F1 11.5 Tf 56 739.7299999999999 Td (horario de apertura y capacidad de reserva o solicitud de pedido.) Tj ET
BT /F1 11.5 Tf 56 723.0099999999999 Td (Cuando existe disponibilidad, el sistema muestra una lista priorizada de farmacias y) Tj ET
BT /F1 11.5 Tf 56 708.2899999999998 Td (puede iniciar una reserva o una indicación de recogida. Cuando no existe) Tj ET
BT /F1 11.5 Tf 56 693.5699999999998 Td (disponibilidad, el sistema no se limita a informar de la falta de stock, sino que) Tj ET
BT /F1 11.5 Tf 56 678.8499999999998 Td (genera una acción técnica adicional, por ejemplo alerta a la farmacia seleccionada,) Tj ET
BT /F1 11.5 Tf 56 664.1299999999998 Td (solicitud de pedido a distribución o proveedor, derivación automática a otra) Tj ET
BT /F1 11.5 Tf 56 649.4099999999997 Td (farmacia con stock o registro de una lista de espera o interés de suministro.) Tj ET
BT /F2 11.5 Tf 56 632.6899999999997 Td (6.3 Tratamiento de datos y seguridad) Tj ET
BT /F1 11.5 Tf 56 617.9699999999997 Td (La invención prevé que la información derivada del escaneo de la TSI se trate con) Tj ET
BT /F1 11.5 Tf 56 603.2499999999997 Td (medidas de minimización de datos, control de acceso, cifrado en tránsito y registro) Tj ET
BT /F1 11.5 Tf 56 588.5299999999996 Td (de eventos. El sistema no expone innecesariamente los datos de la tarjeta sanitaria) Tj ET
BT /F1 11.5 Tf 56 573.8099999999996 Td (y solo conserva los elementos imprescindibles para la operación descrita.) Tj ET
BT /F2 11.5 Tf 56 557.0899999999996 Td (6.4 Integración con la plataforma FarmaFinder) Tj ET
BT /F1 11.5 Tf 56 542.3699999999995 Td (En una implementación concreta, el sistema puede integrarse con una plataforma que) Tj ET
BT /F1 11.5 Tf 56 527.6499999999995 Td (ya disponga de catálogo de farmacias, geolocalización, stock por medicamento,) Tj ET
BT /F1 11.5 Tf 56 512.9299999999995 Td (autenticación de usuario, notificaciones push e histórico de acciones y) Tj ET
BT /F1 11.5 Tf 56 498.20999999999947 Td (preferencias.) Tj ET
BT /F1 11.5 Tf 56 481.48999999999944 Td (La integración permite que el resultado del escaneo de la TSI no se limite a una) Tj ET
BT /F1 11.5 Tf 56 466.7699999999994 Td (consulta pasiva, sino que se convierta en un disparador de decisiones técnicas sobre) Tj ET
BT /F1 11.5 Tf 56 452.0499999999994 Td (disponibilidad y suministro.) Tj ET
BT /F2 13 Tf 56 433.32999999999936 Td (7. Ejemplo de realización) Tj ET
0.2 w 56 412.6899999999994 m 539.28 412.6899999999994 l S
BT /F1 11.2 Tf 64 402.6899999999994 Td (1. El usuario inicia sesión en la aplicación.) Tj ET
BT /F1 11.2 Tf 64 388.35399999999936 Td (2. El usuario escanea su TSI con la cámara del terminal.) Tj ET
BT /F1 11.2 Tf 64 374.01799999999935 Td (3. El sistema extrae el identificador necesario y lo vincula al perfil autorizado.) Tj ET
BT /F1 11.2 Tf 64 359.68199999999933 Td (4. El motor terapéutico asocia una necesidad de medicamento o tratamiento.) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 4) Tj ET
endstream
endobj
8 0 obj
<< /Length 3689 >>
stream
BT /F2 13 Tf 56 781.89 Td (7. Ejemplo de realización \(continuación\)) Tj ET
0.2 w 56 761.25 m 539.28 761.25 l S
BT /F1 11.2 Tf 64 751.25 Td (1. El sistema consulta farmacias cercanas con stock del medicamento.) Tj ET
BT /F1 11.2 Tf 64 736.914 Td (2. Si hay stock, ordena las farmacias por proximidad y disponibilidad.) Tj ET
BT /F1 11.2 Tf 64 722.578 Td (3. Si no hay stock, genera una reserva, una alerta o una solicitud de pedido.) Tj ET
BT /F2 13 Tf 56 706.242 Td (8. Reivindicaciones) Tj ET
0.2 w 56 685.602 m 539.28 685.602 l S
BT /F1 11.2 Tf 56 675.602 Td (Reivindicación 1. Sistema implementado por ordenador para identificar una necesidad) Tj ET
BT /F1 11.2 Tf 56 661.266 Td (terapéutica a partir del escaneo de una tarjeta sanitaria individual, caracterizado) Tj ET
BT /F1 11.2 Tf 56 646.93 Td (porque comprende medios de captura para obtener datos de la tarjeta sanitaria) Tj ET
BT /F1 11.2 Tf 56 632.5939999999999 Td (individual; medios de verificación para asociar dichos datos a un usuario autenticado;) Tj ET
BT /F1 11.2 Tf 56 618.2579999999999 Td (medios de obtención de una referencia de medicamento o necesidad terapéutica; medios) Tj ET
BT /F1 11.2 Tf 56 603.9219999999999 Td (de consulta de inventario para recuperar stock de farmacias geolocalizadas; medios de) Tj ET
BT /F1 11.2 Tf 56 589.5859999999999 Td (contraste para comparar la referencia de medicamento o necesidad terapéutica con el) Tj ET
BT /F1 11.2 Tf 56 575.2499999999999 Td (stock disponible; y medios de generación de acciones para producir una reserva, aviso,) Tj ET
BT /F1 11.2 Tf 56 560.9139999999999 Td (pedido o derivación cuando el stock disponible sea insuficiente.) Tj ET
BT /F1 11.2 Tf 56 544.5779999999999 Td (Reivindicación 2. Sistema según la reivindicación 1, en el que los medios de consulta) Tj ET
BT /F1 11.2 Tf 56 530.2419999999998 Td (de inventario ordenan las farmacias según una combinación de stock disponible,) Tj ET
BT /F1 11.2 Tf 56 515.9059999999998 Td (proximidad geográfica y horario de apertura.) Tj ET
BT /F1 11.2 Tf 56 499.5699999999998 Td (Reivindicación 3. Sistema según cualquiera de las reivindicaciones anteriores, en el) Tj ET
BT /F1 11.2 Tf 56 485.2339999999998 Td (que los datos de la tarjeta sanitaria individual son tratados mediante un módulo de) Tj ET
BT /F1 11.2 Tf 56 470.8979999999998 Td (minimización y cifrado para limitar su almacenamiento a los campos técnicamente) Tj ET
BT /F1 11.2 Tf 56 456.5619999999998 Td (necesarios.) Tj ET
BT /F1 11.2 Tf 56 440.2259999999998 Td (Reivindicación 4. Procedimiento implementado por ordenador para asistir en la) Tj ET
BT /F1 11.2 Tf 56 425.88999999999976 Td (localización y suministro de medicamentos, caracterizado porque comprende escanear una) Tj ET
BT /F1 11.2 Tf 56 411.55399999999975 Td (tarjeta sanitaria individual; verificar la autorización del usuario; derivar una) Tj ET
BT /F1 11.2 Tf 56 397.21799999999973 Td (necesidad terapéutica o referencia de medicamento; consultar stock de farmacias) Tj ET
BT /F1 11.2 Tf 56 382.8819999999997 Td (geolocalizadas; comparar la necesidad con el stock disponible; y generar una acción) Tj ET
BT /F1 11.2 Tf 56 368.5459999999997 Td (técnica de reserva, aviso, pedido o derivación cuando no exista disponibilidad) Tj ET
BT /F1 11.2 Tf 56 354.2099999999997 Td (suficiente.) Tj ET
BT /F1 11.2 Tf 56 337.8739999999997 Td (Reivindicación 5. Dispositivo móvil, terminal o sistema de lectura que comprende una) Tj ET
BT /F1 11.2 Tf 56 323.53799999999967 Td (cámara o lector óptico y que está configurado para ejecutar el sistema de cualquiera) Tj ET
BT /F1 11.2 Tf 56 309.20199999999966 Td (de las reivindicaciones anteriores.) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 5) Tj ET
endstream
endobj
9 0 obj
<< /Length 4216 >>
stream
BT /F2 13 Tf 56 781.89 Td (9. Aplicación industrial) Tj ET
0.2 w 56 761.25 m 539.28 761.25 l S
BT /F1 11.5 Tf 56 751.25 Td (La invención es susceptible de aplicación industrial en plataformas de asistencia) Tj ET
BT /F1 11.5 Tf 56 736.53 Td (farmacéutica, sistemas de gestión de inventario, servicios de reserva o pedido de) Tj ET
BT /F1 11.5 Tf 56 721.81 Td (medicamentos y terminales de consulta sanitaria.) Tj ET
BT /F2 13 Tf 56 703.0899999999999 Td (10. Resumen) Tj ET
0.2 w 56 682.4499999999999 m 539.28 682.4499999999999 l S
BT /F1 11.5 Tf 56 672.4499999999999 Td (La presente invención se refiere a un sistema y procedimiento implementados por) Tj ET
BT /F1 11.5 Tf 56 657.7299999999999 Td (ordenador que permiten, a partir del escaneo de una tarjeta sanitaria individual) Tj ET
BT /F1 11.5 Tf 56 643.0099999999999 Td (mediante un terminal móvil o dispositivo equivalente, asociar de forma segura un) Tj ET
BT /F1 11.5 Tf 56 628.2899999999998 Td (contexto de paciente a una necesidad terapéutica, obtener una referencia de) Tj ET
BT /F1 11.5 Tf 56 613.5699999999998 Td (medicamentos relevantes, comparar dicha necesidad con el stock disponible en) Tj ET
BT /F1 11.5 Tf 56 598.8499999999998 Td (farmacias geolocalizadas y generar, en caso de falta de disponibilidad, una acción) Tj ET
BT /F1 11.5 Tf 56 584.1299999999998 Td (de reserva, aviso, solicitud de pedido o derivación a una farmacia alternativa. El) Tj ET
BT /F1 11.5 Tf 56 569.4099999999997 Td (sistema integra captura de datos, verificación de identidad, geolocalización,) Tj ET
BT /F1 11.5 Tf 56 554.6899999999997 Td (consulta de disponibilidad, gestión de stock y generación de eventos de suministro) Tj ET
BT /F1 11.5 Tf 56 539.9699999999997 Td (en una única cadena técnica de procesamiento.) Tj ET
BT /F2 13 Tf 56 521.2499999999997 Td (11. Anexo de dibujos) Tj ET
0.2 w 56 500.6099999999997 m 539.28 500.6099999999997 l S
BT /F1 11.5 Tf 56 490.6099999999997 Td (Las figuras indicadas en la presente memoria tienen carácter esquemático y se) Tj ET
BT /F1 11.5 Tf 56 475.88999999999965 Td (incorporan a efectos ilustrativos de la arquitectura, del flujo de captura, del) Tj ET
BT /F1 11.5 Tf 56 461.1699999999996 Td (contraste de stock y de la generación de acciones. En una presentación definitiva,) Tj ET
BT /F1 11.5 Tf 56 446.4499999999996 Td (dichas figuras podrán acompañarse como láminas separadas numeradas de forma) Tj ET
BT /F1 11.5 Tf 56 431.72999999999956 Td (consecutiva.) Tj ET
BT /F2 13 Tf 56 413.00999999999954 Td (12. Firma del solicitante) Tj ET
0.2 w 56 392.36999999999955 m 539.28 392.36999999999955 l S
BT /F1 11.5 Tf 56 382.36999999999955 Td (En Terrassa, a 22 de junio de 2026.) Tj ET
BT /F1 11.5 Tf 56 365.6499999999995 Td (Fdo.: Antoni Nuñez Romeu) Tj ET
BT /F1 11.5 Tf 56 348.9299999999995 Td (DNI/NIF: 45858029P) Tj ET
BT /F2 13 Tf 56 330.20999999999947 Td (13. Notas para presentación en OEPM) Tj ET
0.2 w 56 309.5699999999995 m 539.28 309.5699999999995 l S
BT /F1 11.2 Tf 64 299.5699999999995 Td (" La solicitud debe presentarse como una única invención o un único concepto inventivo) Tj ET
BT /F1 11.2 Tf 64 285.23399999999947 Td ( general.) Tj ET
BT /F1 11.2 Tf 64 270.89799999999946 Td (" La descripción, reivindicaciones y dibujos deben ser coherentes entre sí.) Tj ET
BT /F1 11.2 Tf 64 256.56199999999944 Td (" La protección en patente debe apoyarse en un efecto técnico concreto y no en una mera) Tj ET
BT /F1 11.2 Tf 64 242.22599999999943 Td ( regla comercial o método abstracto.) Tj ET
BT /F1 11.2 Tf 64 227.88999999999942 Td (" Esta redacción evita formular una reivindicación independiente de "programa de) Tj ET
BT /F1 11.2 Tf 64 213.5539999999994 Td ( ordenador como tal" y se centra en un sistema y un procedimiento implementados por) Tj ET
BT /F1 11.2 Tf 64 199.2179999999994 Td ( ordenador con interacción técnica sobre captura, contraste de inventario y generación) Tj ET
BT /F1 11.2 Tf 64 184.88199999999938 Td ( de acciones.) Tj ET
BT /F1 11.2 Tf 64 170.54599999999937 Td (" Antes de presentar la solicitud conviene una búsqueda de anterioridades y una revisión) Tj ET
BT /F1 11.2 Tf 64 156.20999999999935 Td ( por agente de propiedad industrial.) Tj ET
BT /F1 9 Tf 495.28 24 Td (Pagina 6) Tj ET
endstream
endobj
10 0 obj
<< /Type /Pages /Kids [11 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R] /Count 6 >>
endobj
11 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 4 0 R >>
endobj
12 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 5 0 R >>
endobj
13 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 6 0 R >>
endobj
14 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 7 0 R >>
endobj
15 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 8 0 R >>
endobj
16 0 obj
<< /Type /Page /Parent 10 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 1 0 R /F2 2 0 R /F3 3 0 R >> >> /Contents 9 0 R >>
endobj
17 0 obj
<< /Type /Catalog /Pages 10 0 R >>
endobj
xref
0 18
0000000000 65535 f
0000000009 00000 n
0000000108 00000 n
0000000206 00000 n
0000000306 00000 n
0000001462 00000 n
0000006464 00000 n
0000009952 00000 n
0000013275 00000 n
0000017016 00000 n
0000021284 00000 n
0000021378 00000 n
0000021532 00000 n
0000021686 00000 n
0000021840 00000 n
0000021994 00000 n
0000022148 00000 n
0000022302 00000 n
trailer << /Size 18 /Root 17 0 R >>
startxref
22353
%%EOF
+2 -2
View File
@@ -2,8 +2,8 @@
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
+3 -1
View File
@@ -10,7 +10,7 @@
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
@@ -41,4 +41,6 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
+2 -2
View File
@@ -7,8 +7,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+2 -1
View File
@@ -1,6 +1,7 @@
#Fri Jun 26 12:32:52 CEST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+13 -13
View File
@@ -1,16 +1,16 @@
ext {
minSdkVersion = 22
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}
+2 -1
View File
@@ -1,6 +1,7 @@
FROM node:18-alpine
FROM node:18-slim
WORKDIR /app
COPY backend/package*.json ./
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
RUN npm ci --omit=dev
COPY backend/ .
COPY API/ /API/
+49
View File
@@ -340,6 +340,55 @@ app.get('/api/pharmacies', async (req, res) => {
}
});
// TSI Card scan: return mock prescriptions for a given CIP
// In production this would integrate with a real health data source
app.get('/api/tsi/:cip/prescriptions', async (req, res) => {
try {
const cip = req.params.cip || '';
// Deterministic prescription set based on CIP prefix — simulates per-patient data
const cipUpper = cip.toUpperCase();
let medicineNames;
if (cipUpper.startsWith('ALPE')) {
medicineNames = ['Paracetamol', 'Omeprazol', 'Ibuprofeno'];
} else if (cipUpper.startsWith('MADS')) {
medicineNames = ['Amoxicilina', 'Loratadina', 'Ibuprofeno'];
} else if (cipUpper.startsWith('VALE')) {
medicineNames = ['Atorvastatina', 'Metformina', 'Losartán'];
} else {
medicineNames = ['Paracetamol', 'Ibuprofeno'];
}
// Build a static prescription list (CIMA API mock)
const prescriptions = medicineNames.map((name, idx) => {
const dosages = ['500mg', '20mg', '600mg', '500mg', '850mg'];
const forms = ['Comprimidos', 'Cápsulas', 'Comprimidos recubiertos', 'Cápsulas', 'Comprimidos'];
const actives = {
'Paracetamol': 'Paracetamol',
'Omeprazol': 'Omeprazol',
'Ibuprofeno': 'Ibuprofeno',
'Amoxicilina': 'Amoxicilina',
'Loratadina': 'Loratadina',
'Atorvastatina': 'Atorvastatina',
'Metformina': 'Metformina',
'Losartán': 'Losartán potásico',
};
return {
id: `RX_${cip}_${idx}`,
name: name,
active_ingredient: actives[name] || name,
dosage: dosages[idx % dosages.length],
form: forms[idx % forms.length],
};
});
res.json(prescriptions);
} catch (error) {
console.error('Error fetching TSI prescriptions:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// ========== AUTHENTICATION MIDDLEWARE ==========
// Middleware to check if user is authenticated
+198
View File
@@ -0,0 +1,198 @@
---
format_version: "11"
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: capacitor
app:
envs:
- BITRISE_PROJECT_PATH: ""
- BITRISE_SCHEME: ""
trigger_map:
- push_branch: main
workflow: deploy
- pull_request_source_branch: "*"
workflow: test
- tag: "*"
workflow: deploy
workflows:
# ── Shared setup ──────────────────────────────────────────
_setup:
steps:
- activate-ssh-key@4:
run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
- git-clone@8: {}
- nvm@1:
inputs:
- node_version: "20"
- script@1:
title: Install dependencies & build frontend
inputs:
- content: |
#!/bin/bash
set -ex
npm install
npm install --prefix frontend
npm run build --prefix frontend
# ── Android test / PR build ──────────────────────────────
test:
before_run:
- _setup
steps:
- script@1:
title: Sync Android native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync android
- script@1:
title: Install Android SDK components
inputs:
- content: |
#!/bin/bash
set -ex
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
"platforms;android-36" \
"build-tools;36.0.0" \
"platform-tools" || true
- android-build@1:
title: Build Android APK (debug)
inputs:
- project_location: android
- module: app
- variant: debug
# ── Android deploy ───────────────────────────────────────
deploy_android:
before_run:
- _setup
steps:
- script@1:
title: Sync Android native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync android
- script@1:
title: Install Android SDK components
inputs:
- content: |
#!/bin/bash
set -ex
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
"platforms;android-36" \
"build-tools;36.0.0" \
"platform-tools" || true
- android-build@1:
title: Build Android AAB (release)
inputs:
- project_location: android
- module: app
- variant: release
- deploy-to-bitrise-io@2: {}
# ── iOS test / PR build ─────────────────────────────────
test_ios:
before_run:
- _setup
steps:
- script@1:
title: Sync iOS native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync ios
- cocoapods-install@2:
inputs:
- podfile_path: ios/App/Podfile
- xcode-archive@4:
title: Build iOS (simulator)
inputs:
- project_path: ios/App/App.xcodeproj
- scheme: App
- configuration: Debug
- destination: platform=iOS Simulator,name=iPhone 15,OS=latest
- output_tool: xcodebuild
# ── iOS deploy ───────────────────────────────────────────
deploy_ios:
before_run:
- _setup
steps:
- script@1:
title: Sync iOS native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync ios
- cocoapods-install@2:
inputs:
- podfile_path: ios/App/Podfile
- xcode-archive@4:
title: Archive iOS (release)
inputs:
- project_path: ios/App/App.xcodeproj
- scheme: App
- configuration: Release
- destination: generic/platform=iOS
- output_tool: xcodebuild
- export_method: app-store
- deploy-to-bitrise-io@2: {}
# ── Full deploy (both platforms) ─────────────────────────
deploy:
before_run:
- _setup
steps:
# Android
- script@1:
title: Sync Android native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync android
- script@1:
title: Install Android SDK components
inputs:
- content: |
#!/bin/bash
set -ex
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
"platforms;android-36" \
"build-tools;36.0.0" \
"platform-tools" || true
- android-build@1:
title: Build Android AAB (release)
inputs:
- project_location: android
- module: app
- variant: release
# iOS
- script@1:
title: Sync iOS native project
inputs:
- content: |
#!/bin/bash
set -ex
npx cap sync ios
- cocoapods-install@2:
inputs:
- podfile_path: ios/App/Podfile
- xcode-archive@4:
title: Archive iOS (release)
inputs:
- project_path: ios/App/App.xcodeproj
- scheme: App
- configuration: Release
- destination: generic/platform=iOS
- output_tool: xcodebuild
- export_method: app-store
- deploy-to-bitrise-io@2: {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

+90 -24
View File
@@ -8,10 +8,12 @@
"name": "farma-finder-frontend",
"version": "1.0.0",
"dependencies": {
"@capacitor/app": "^6.0.3",
"@capacitor/core": "^6.2.1",
"@capacitor/splash-screen": "^6.0.4",
"@capacitor/status-bar": "^6.0.3",
"@capacitor-mlkit/barcode-scanning": "^8.1.0",
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.1",
"@capacitor/splash-screen": "^8.0.1",
"@capacitor/status-bar": "^8.0.2",
"barcode-detector": "^3.2.0",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1646,40 +1648,54 @@
"node": ">=6.9.0"
}
},
"node_modules/@capacitor/app": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/app/-/app-6.0.3.tgz",
"integrity": "sha512-4gFUCbcVz0N/YYN32OBFerocWXslIv3Nc90gDiRsBkJc0plwK6kIUT6PKa5WtW2kfhteUeCVXQbvArH2fH+0Ug==",
"license": "MIT",
"node_modules/@capacitor-mlkit/barcode-scanning": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@capacitor-mlkit/barcode-scanning/-/barcode-scanning-8.1.0.tgz",
"integrity": "sha512-lhOYHZINLOCT0i5YSbSMkouik3zh0BncJouumNTgXCT0/533Z4733jAX9zv+nEd++bE8QHeUl2g0NrewreumnQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/capawesome-team/"
},
{
"type": "opencollective",
"url": "https://opencollective.com/capawesome"
}
],
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/app": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz",
"integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==",
"peerDependencies": {
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/core": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
"license": "MIT",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.1.tgz",
"integrity": "sha512-xqhOGLbTAYeOWK+IDUNSjQJAPapQjRHrIcgk9PYp52or9zFTaaMko31uNi16N6W+CRJ8VrRram6fOYILkBG2Hg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/splash-screen": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz",
"integrity": "sha512-uJXR+28cdaie7zIIUBvgkWgHim6Gr1itJym9voIMTmrjXkOaPtejwxYJsdQWPJz9zgGnSbXuC1mNNibLgv3OpQ==",
"license": "MIT",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.1.tgz",
"integrity": "sha512-c/ew/Z3eA7z8l06WoRAtzVF16VwYYrExmHmfGq1Cg675pVzaC/yuucB8/1xG1vhEfnW4fZ1KhSf/kzR1RiVYgg==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.3.tgz",
"integrity": "sha512-nFlgSmtx6Zwaw0tEvZgQsWHBeOfWWB/AvEoCApopLT4mHkBVoSrwkLvy2PjZs5wxCbsmqvQczr3XCyTwaDZVQg==",
"license": "MIT",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
"integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@csstools/color-helpers": {
@@ -2875,6 +2891,11 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/emscripten": {
"version": "1.41.5",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3331,6 +3352,14 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/barcode-detector": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-3.2.0.tgz",
"integrity": "sha512-MrT5TT058ptG5YB157pHLfXKVpp0BKEfQBOb8QvzTbatzmLDu85JJ0Gd/sCYwbwdwStJvxsYflrSN6D6E4Ndyw==",
"dependencies": {
"zxing-wasm": "3.1.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
@@ -6805,6 +6834,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@@ -7833,6 +7873,32 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zxing-wasm": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-3.1.0.tgz",
"integrity": "sha512-5+3V1wPRx4gvbeLH2jB7n2cKrYJ1q4i3QgjnBUtrDPeqxJSi6BdzKJg4y6aF6bgW8zfntnYJyrkqFMevDhL2NA==",
"dependencies": {
"@types/emscripten": "^1.41.5",
"type-fest": "^5.7.0"
},
"peerDependencies": {
"@types/emscripten": ">=1.39.6"
}
},
"node_modules/zxing-wasm/node_modules/type-fest": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz",
"integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+6 -4
View File
@@ -9,10 +9,12 @@
"test": "vitest"
},
"dependencies": {
"@capacitor/app": "^6.0.3",
"@capacitor/core": "^6.2.1",
"@capacitor/splash-screen": "^6.0.4",
"@capacitor/status-bar": "^6.0.3",
"@capacitor-mlkit/barcode-scanning": "^8.1.0",
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.1",
"@capacitor/splash-screen": "^8.0.1",
"@capacitor/status-bar": "^8.0.2",
"barcode-detector": "^3.2.0",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+22
View File
@@ -66,6 +66,28 @@
text-align: center;
margin-bottom: 4rem;
animation: fadeInDown 0.8s ease-out;
position: relative;
}
.back-to-home-btn {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: var(--surface-muted);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.88rem;
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 999px;
cursor: pointer;
transition: color 0.2s, background 0.2s;
}
.back-to-home-btn:hover {
color: var(--text-main);
background: var(--surface-card);
}
.app-header h1 {
+26 -2
View File
@@ -11,9 +11,29 @@ afterEach(() => {
vi.useRealTimers()
})
describe('PublicView', () => {
it('renders search bar on load with no results', () => {
// Helper: navigate from HomeView to the search screen
async function navigateToSearch() {
fireEvent.click(screen.getByRole('button', { name: /manual search/i }))
}
describe('HomeView', () => {
it('renders two action buttons on the home screen', () => {
render(<PublicView />)
expect(screen.getByRole('button', { name: /scan tsi/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /manual search/i })).toBeInTheDocument()
})
it('navigates to search screen when Manual Search is clicked', async () => {
render(<PublicView />)
await navigateToSearch()
expect(screen.getByPlaceholderText(/search for a medicine/i)).toBeInTheDocument()
})
})
describe('PublicView (search screen)', () => {
it('renders search bar on load with no results', async () => {
render(<PublicView />)
await navigateToSearch()
expect(screen.getByPlaceholderText(/search for a medicine/i)).toBeInTheDocument()
expect(screen.queryByRole('list')).not.toBeInTheDocument()
})
@@ -21,6 +41,7 @@ describe('PublicView', () => {
it('does not fetch for queries shorter than 2 chars', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch')
render(<PublicView />)
await navigateToSearch()
fireEvent.change(screen.getByPlaceholderText(/search for a medicine/i), {
target: { value: 'a' },
@@ -42,6 +63,7 @@ describe('PublicView', () => {
})
render(<PublicView />)
await navigateToSearch()
fireEvent.change(screen.getByPlaceholderText(/search for a medicine/i), {
target: { value: 'ibu' },
@@ -63,6 +85,7 @@ describe('PublicView', () => {
})
render(<PublicView />)
await navigateToSearch()
const input = screen.getByPlaceholderText(/search for a medicine/i)
fireEvent.change(input, { target: { value: 'ibu' } })
@@ -75,3 +98,4 @@ describe('PublicView', () => {
expect(screen.queryByText('Ibuprofeno 400mg')).not.toBeInTheDocument()
})
})
+292
View File
@@ -0,0 +1,292 @@
/* ============================================================
HomeView.css — Premium home page styles
============================================================ */
.home-container {
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem 1.5rem calc(5rem + env(safe-area-inset-bottom));
gap: 2.5rem;
animation: fadeInUp 0.7s ease-out;
}
/* ── Header ─────────────────────────────────────────────── */
.home-header {
text-align: center;
max-width: 34rem;
}
.home-logo {
font-size: clamp(2.6rem, 7vw, 4rem);
font-weight: 900;
color: var(--text-main);
letter-spacing: -0.04em;
line-height: 1;
margin-bottom: 0.9rem;
}
.home-logo::after {
content: "";
display: block;
width: 3rem;
height: 4px;
margin: 0.85rem auto 0;
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
border-radius: 2px;
}
.home-subtitle {
font-size: 1.1rem;
color: var(--text-muted);
font-weight: 400;
line-height: 1.55;
}
/* ── Card Grid ──────────────────────────────────────────── */
.home-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
width: 100%;
max-width: 54rem;
}
/* ── Base Card ──────────────────────────────────────────── */
.home-card {
position: relative;
display: flex;
flex-direction: column;
gap: 1.2rem;
padding: 2rem 1.75rem 1.6rem;
border-radius: 20px;
border: 1px solid var(--glass-border);
background: var(--glass-bg);
box-shadow: var(--glass-shadow);
cursor: pointer;
text-align: left;
overflow: hidden;
transition:
transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.25s ease,
border-color 0.25s ease;
}
.home-card::before {
content: "";
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: inherit;
pointer-events: none;
}
.home-card:hover {
transform: translateY(-5px) scale(1.015);
box-shadow:
0 16px 40px rgba(28, 25, 23, 0.14),
0 4px 12px rgba(28, 25, 23, 0.08);
}
.home-card:hover::before {
opacity: 1;
}
.home-card:active {
transform: translateY(-2px) scale(1.005);
}
/* ── Scan Card Accent ───────────────────────────────────── */
.scan-card {
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.97) 0%,
rgba(240, 253, 250, 0.97) 100%
);
border-color: rgba(15, 118, 110, 0.22);
}
.scan-card::before {
background: radial-gradient(
ellipse at top left,
rgba(15, 118, 110, 0.08) 0%,
transparent 70%
);
}
.scan-card:hover {
border-color: rgba(15, 118, 110, 0.45);
box-shadow:
0 16px 40px rgba(15, 118, 110, 0.18),
0 4px 12px rgba(15, 118, 110, 0.12);
}
/* ── Search Card Accent ─────────────────────────────────── */
.search-card {
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.97) 0%,
rgba(245, 245, 244, 0.97) 100%
);
}
.search-card:hover {
border-color: rgba(71, 85, 105, 0.35);
box-shadow:
0 16px 40px rgba(28, 25, 23, 0.12),
0 4px 12px rgba(28, 25, 23, 0.06);
}
/* ── Badge ──────────────────────────────────────────────── */
.card-badge {
position: absolute;
top: 1.1rem;
right: 1.1rem;
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
color: #fff;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.28rem 0.65rem;
border-radius: 999px;
box-shadow: 0 2px 8px rgba(15, 118, 110, 0.35);
}
/* ── Icon Container ─────────────────────────────────────── */
.card-icon-container {
position: relative;
width: 4rem;
height: 4rem;
display: flex;
align-items: center;
justify-content: center;
}
.card-icon {
font-size: 2.4rem;
line-height: 1;
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.12));
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.home-card:hover .card-icon {
transform: scale(1.18) rotate(-4deg);
}
/* Pulse rings on the Scan card */
.pulse-ring,
.pulse-ring-outer {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid rgba(15, 118, 110, 0.4);
animation: pulse-ring 2.2s ease-out infinite;
pointer-events: none;
}
.pulse-ring-outer {
animation-delay: 0.7s;
border-color: rgba(15, 118, 110, 0.2);
}
@keyframes pulse-ring {
0% { transform: scale(0.85); opacity: 0.7; }
70% { transform: scale(1.6); opacity: 0; }
100% { transform: scale(1.6); opacity: 0; }
}
/* ── Card Content ───────────────────────────────────────── */
.card-content h3 {
font-size: 1.4rem;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
}
.card-content p {
font-size: 0.95rem;
color: var(--text-muted);
line-height: 1.55;
}
/* ── Card Action Footer ─────────────────────────────────── */
.card-action {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 0.8rem;
border-top: 1px solid var(--border);
font-size: 0.9rem;
font-weight: 600;
color: var(--primary);
}
.scan-card .card-action {
color: var(--primary);
}
.search-card .card-action {
color: var(--text-muted);
}
.search-card:hover .card-action {
color: var(--text-main);
}
.action-arrow {
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
font-size: 1.1rem;
}
.home-card:hover .action-arrow {
transform: translateX(5px);
}
/* ── Footer ─────────────────────────────────────────────── */
.home-footer {
max-width: 40rem;
text-align: center;
}
.home-footer p {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.55;
opacity: 0.8;
}
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 640px) {
.home-container {
padding: 1.5rem 1rem calc(6rem + env(safe-area-inset-bottom));
gap: 1.75rem;
justify-content: flex-start;
padding-top: 2.5rem;
}
.home-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.home-card {
padding: 1.5rem 1.4rem 1.3rem;
gap: 0.9rem;
}
.card-content h3 {
font-size: 1.25rem;
}
.card-content p {
font-size: 0.88rem;
}
}
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
import './HomeView.css';
function HomeView({ onScanClick, onSearchClick }) {
return (
<div className="home-container">
<header className="home-header">
<h1 className="home-logo">💊 FarmaFinder</h1>
<p className="home-subtitle">Find your medicine at nearby pharmacies quickly and easily.</p>
</header>
<div className="home-grid">
<button className="home-card scan-card" onClick={onScanClick} aria-label="Scan TSI Card">
<div className="card-badge">Fast Access</div>
<div className="card-icon-container">
<span className="card-icon">📷</span>
<div className="pulse-ring"></div>
<div className="pulse-ring-outer"></div>
</div>
<div className="card-content">
<h3>Scan TSI Card</h3>
<p>Scan your health card (Tarjeta Sanitaria Individual) barcode or QR code with your camera to instantly view your active prescriptions.</p>
</div>
<div className="card-action">
<span>Scan now</span>
<span className="action-arrow"></span>
</div>
</button>
<button className="home-card search-card" onClick={onSearchClick} aria-label="Manual Search">
<div className="card-icon-container">
<span className="card-icon">🔍</span>
</div>
<div className="card-content">
<h3>Manual Search</h3>
<p>Search for a specific medicine by typing its name, active ingredient, or register number to check availability in real-time.</p>
</div>
<div className="card-action">
<span>Search catalogue</span>
<span className="action-arrow"></span>
</div>
</button>
</div>
<footer className="home-footer">
<p>🔒 Your medical and personal data is secure. FarmaFinder complies with data protection regulations and does not store sensitive card information.</p>
</footer>
</div>
);
}
export default HomeView;
+45
View File
@@ -4,9 +4,14 @@ import SearchBar from '../components/SearchBar';
import MedicineResults from '../components/MedicineResults';
import PharmacyList from '../components/PharmacyList';
import PharmacyMap from '../components/PharmacyMap';
import HomeView from './HomeView';
import ScannerView from './ScannerView';
import { haversineKm, getUserPosition } from '../utils/geo';
function PublicView({ currentUser, onLoginRequest }) {
// 'home' | 'scan' | 'search'
const [screen, setScreen] = useState('home');
const [searchQuery, setSearchQuery] = useState('');
const [medicines, setMedicines] = useState([]);
const [selectedMedicine, setSelectedMedicine] = useState(null);
@@ -120,9 +125,49 @@ function PublicView({ currentUser, onLoginRequest }) {
});
}, [pharmacies, sortByDistance, userPosition]);
/* ── Scanner → Search handoff ──────────────────────────── */
function handleScanSelectMedicine(medicineName) {
setSearchQuery(medicineName);
setSelectedMedicine(null);
setPharmacies([]);
setScreen('search');
}
/* ── Screens ─────────────────────────────────────────────── */
if (screen === 'home') {
return (
<HomeView
onScanClick={() => setScreen('scan')}
onSearchClick={() => setScreen('search')}
/>
);
}
if (screen === 'scan') {
return (
<ScannerView
onClose={() => setScreen('home')}
onSelectMedicine={handleScanSelectMedicine}
/>
);
}
// screen === 'search'
return (
<>
<header className="app-header">
<button
className="back-to-home-btn"
onClick={() => {
setSearchQuery('');
setSelectedMedicine(null);
setPharmacies([]);
setScreen('home');
}}
aria-label="Back to home"
>
Home
</button>
<h1>💊 FarmaFinder</h1>
<p>Find your medicine at nearby pharmacies</p>
</header>
+440
View File
@@ -0,0 +1,440 @@
/* ============================================================
ScannerView.css — Barcode scanning interface styles
============================================================ */
/* ── Full-screen Overlay ─────────────────────────────────── */
.scanner-overlay {
position: fixed;
inset: 0;
background: #0a0e17;
z-index: 200;
display: flex;
flex-direction: column;
overflow-y: auto;
overscroll-behavior: contain;
animation: fadeInFast 0.3s ease-out;
}
@keyframes fadeInFast {
from { opacity: 0; }
to { opacity: 1; }
}
/* ── Top Bar ─────────────────────────────────────────────── */
.scanner-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.2rem calc(1rem + env(safe-area-inset-top));
padding-top: calc(1rem + env(safe-area-inset-top));
background: rgba(10, 14, 23, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255,255,255,0.07);
position: sticky;
top: 0;
z-index: 10;
flex-shrink: 0;
}
.scanner-back-btn {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
color: #e2e8f0;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s;
}
.scanner-back-btn:hover {
background: rgba(255,255,255,0.14);
}
.scanner-title {
font-size: 1.05rem;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.01em;
}
/* ── Content Area ────────────────────────────────────────── */
.scanner-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 1.5rem 1.25rem calc(2rem + env(safe-area-inset-bottom));
width: 100%;
max-width: 42rem;
margin: 0 auto;
}
/* ── Start Scan Button ───────────────────────────────────── */
.scan-start-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
width: 100%;
background: linear-gradient(135deg, #0f766e, #0d9488);
color: #fff;
border: none;
border-radius: 16px;
padding: 1.1rem 1.5rem;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
letter-spacing: -0.01em;
box-shadow: 0 4px 20px rgba(15, 118, 110, 0.45);
transition: transform 0.2s, box-shadow 0.2s;
}
.scan-start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(15, 118, 110, 0.55);
}
.scan-start-btn:active {
transform: translateY(0);
}
.scan-start-icon {
font-size: 1.5rem;
}
/* ── Manual CIP Input ────────────────────────────────────── */
.manual-cip-section {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.cip-label {
font-size: 0.8rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.cip-input-group {
display: flex;
gap: 0.5rem;
}
.cip-input {
flex: 1;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
padding: 0.85rem 1rem;
font-size: 1rem;
font-family: 'Courier New', monospace;
color: #e2e8f0;
letter-spacing: 0.08em;
outline: none;
transition: border-color 0.2s;
}
.cip-input::placeholder {
color: #475569;
letter-spacing: 0.04em;
font-family: system-ui, sans-serif;
}
.cip-input:focus {
border-color: rgba(16, 185, 129, 0.5);
}
.cip-submit-btn {
background: linear-gradient(135deg, #0f766e, #0d9488);
color: #fff;
border: none;
border-radius: 12px;
padding: 0.85rem 1.4rem;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: transform 0.2s, box-shadow 0.2s;
}
.cip-submit-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(15, 118, 110, 0.4);
}
/* ── PWA Notice ──────────────────────────────────────────── */
.pwa-notice {
display: flex;
align-items: flex-start;
gap: 0.75rem;
background: rgba(59, 130, 246, 0.08);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 14px;
padding: 1rem 1.2rem;
}
.pwa-notice-icon {
font-size: 1.3rem;
flex-shrink: 0;
margin-top: 0.1rem;
}
.pwa-notice p {
margin: 0;
font-size: 0.9rem;
color: #93c5fd;
line-height: 1.5;
}
/* ── Error Panel ─────────────────────────────────────────── */
.scan-error-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
background: rgba(239, 68, 68, 0.06);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 16px;
padding: 1.5rem 1.2rem;
text-align: center;
}
.scan-error-icon {
font-size: 2.5rem;
}
.scan-error-msg {
color: #fca5a5;
font-size: 0.95rem;
line-height: 1.5;
margin: 0;
}
.scan-retry-btn {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
border-radius: 10px;
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.scan-retry-btn:hover {
background: rgba(239, 68, 68, 0.25);
}
/* ── Scanning Active Panel (native) ──────────────────────── */
.scan-active-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
}
.scan-active-panel p {
color: #94a3b8;
font-size: 0.95rem;
margin: 0;
}
/* ── Camera Viewport (web PWA scanning) ──────────────────── */
.scanner-camera-container {
position: relative;
width: 100%;
border-radius: 16px;
overflow: hidden;
background: #000;
aspect-ratio: 4/3;
}
.scanner-video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.scanner-frame {
position: absolute;
inset: 10% 12%;
pointer-events: none;
}
.corner {
position: absolute;
width: 22px;
height: 22px;
border-color: #10b981;
border-style: solid;
border-width: 0;
}
.corner.tl { top: 0; left: 0; border-top-width: 3px; border-left-width: 3px; border-radius: 4px 0 0 0; }
.corner.tr { top: 0; right: 0; border-top-width: 3px; border-right-width: 3px; border-radius: 0 4px 0 0; }
.corner.bl { bottom: 0; left: 0; border-bottom-width: 3px; border-left-width: 3px; border-radius: 0 0 0 4px; }
.corner.br { bottom: 0; right: 0; border-bottom-width: 3px; border-right-width: 3px; border-radius: 0 0 4px 0; }
.scanner-hint {
position: absolute;
bottom: 0.75rem;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-size: 0.78rem;
color: rgba(255,255,255,0.75);
background: rgba(0,0,0,0.55);
backdrop-filter: blur(6px);
padding: 0.3rem 0.75rem;
border-radius: 999px;
}
/* ── Spinner ─────────────────────────────────────────────── */
.scanner-spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(255,255,255,0.12);
border-top-color: #10b981;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ── Prescriptions Panel ─────────────────────────────────── */
.scanner-prescriptions {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.1rem;
padding: 1.5rem 1.25rem calc(2rem + env(safe-area-inset-bottom));
width: 100%;
max-width: 42rem;
margin: 0 auto;
animation: fadeInUp 0.4s ease-out;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.scanned-card-info {
display: flex;
align-items: center;
gap: 1rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 14px;
padding: 1rem 1.2rem;
}
.scanned-icon { font-size: 2rem; }
.scanned-cip {
font-size: 0.85rem;
color: #64748b;
font-family: 'Courier New', monospace;
margin: 0;
}
.rx-heading {
font-size: 1.1rem;
font-weight: 800;
color: #f1f5f9;
letter-spacing: -0.02em;
margin: 0;
}
.rx-subheading {
font-size: 0.85rem;
color: #64748b;
margin: -0.5rem 0 0;
}
.rx-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
padding: 2rem 0;
color: #64748b;
font-size: 0.9rem;
}
.rx-empty {
color: #64748b;
text-align: center;
padding: 1.5rem 0;
font-size: 0.9rem;
}
.rx-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rx-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 14px;
padding: 1rem 1.1rem;
cursor: pointer;
width: 100%;
text-align: left;
transition: background 0.2s, border-color 0.2s, transform 0.2s;
}
.rx-item:hover {
background: rgba(255,255,255,0.09);
border-color: rgba(16, 185, 129, 0.35);
transform: translateX(3px);
}
.rx-item-info {
display: flex;
flex-direction: column;
gap: 0.18rem;
}
.rx-name {
font-size: 1rem;
font-weight: 700;
color: #e2e8f0;
}
.rx-detail {
font-size: 0.82rem;
color: #64748b;
}
.rx-ingredient {
font-size: 0.78rem;
color: #10b981;
font-weight: 500;
}
.rx-arrow {
color: #10b981;
font-size: 1.2rem;
flex-shrink: 0;
transition: transform 0.2s;
}
.rx-item:hover .rx-arrow {
transform: translateX(4px);
}
.scan-again-btn {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
color: #94a3b8;
border-radius: 12px;
padding: 0.75rem 1.2rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
margin-top: 0.5rem;
}
.scan-again-btn:hover {
background: rgba(255,255,255,0.1);
color: #e2e8f0;
}
+356
View File
@@ -0,0 +1,356 @@
import React, { useState, useCallback, useRef } from 'react';
import { BarcodeScanner, BarcodeFormat } from '@capacitor-mlkit/barcode-scanning';
import { Capacitor } from '@capacitor/core';
import 'barcode-detector/polyfill';
import './ScannerView.css';
const CIP_REGEX = /^[A-Z0-9]{16}$/i;
function playBeep() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1046, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1318, ctx.currentTime + 0.08);
gain.gain.setValueAtTime(0.28, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.38);
} catch (_) { /* No audio context available */ }
}
function ScannerView({ onClose, onSelectMedicine }) {
const videoRef = useRef(null);
const streamRef = useRef(null);
const rafRef = useRef(null);
const detectorRef = useRef(null);
const [phase, setPhase] = useState('idle');
const [manualCip, setManualCip] = useState('');
const [prescriptions, setPrescriptions] = useState([]);
const [scannedCip, setScannedCip] = useState(null);
const [loadingPrescriptions, setLoadingPrescriptions] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
const isNative = Capacitor.isNativePlatform();
const fetchPrescriptions = useCallback(async (cip) => {
setLoadingPrescriptions(true);
try {
const res = await fetch(`/api/tsi/${encodeURIComponent(cip)}/prescriptions`);
const data = await res.json();
setPrescriptions(data);
} catch {
setPrescriptions([]);
} finally {
setLoadingPrescriptions(false);
}
}, []);
function stopCamera() {
streamRef.current?.getTracks().forEach(t => t.stop());
streamRef.current = null;
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
}
async function handleNativeScan() {
setErrorMsg('');
try {
const supported = await BarcodeScanner.isSupported();
if (!supported) {
setErrorMsg('Barcode scanning is not supported on this device.');
setPhase('error');
return;
}
const permission = await BarcodeScanner.checkPermissions();
if (permission.camera !== 'granted') {
const request = await BarcodeScanner.requestPermissions();
if (request.camera !== 'granted') {
setErrorMsg('Camera permission denied. Please enable it in settings, or enter your CIP code manually.');
setPhase('error');
return;
}
}
setPhase('scanning');
const result = await BarcodeScanner.scan({
formats: [BarcodeFormat.Code128, BarcodeFormat.QrCode],
autoZoom: true,
});
const barcode = result.barcodes[0];
const rawValue = barcode?.rawValue;
if (!rawValue || !CIP_REGEX.test(rawValue)) {
setErrorMsg('Invalid card barcode. Please try again or enter your CIP code manually.');
setPhase('error');
return;
}
playBeep();
setScannedCip(rawValue);
setPhase('prescriptions');
fetchPrescriptions(rawValue);
} catch (err) {
if (err?.message?.includes('cancel') || err?.message?.includes('User')) {
setPhase('idle');
return;
}
setErrorMsg(`Scan failed: ${err.message || 'Unknown error'}`);
setPhase('error');
}
}
async function handleWebScan() {
setErrorMsg('');
try {
if (!navigator.mediaDevices?.getUserMedia) {
setErrorMsg('Camera not available in this browser.');
setPhase('error');
return;
}
let detector;
try {
detector = new BarcodeDetector({ formats: ['code_128', 'qr_code'] });
} catch {
setErrorMsg('Barcode detection is not supported in this browser.');
setPhase('error');
return;
}
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
});
streamRef.current = stream;
detectorRef.current = detector;
setPhase('scanning');
await new Promise((resolve) => {
requestAnimationFrame(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.play().then(resolve).catch(resolve);
} else {
resolve();
}
});
});
let found = false;
async function scanFrame() {
if (found || !videoRef.current || !detectorRef.current) return;
try {
const barcodes = await detectorRef.current.detect(videoRef.current);
if (barcodes.length > 0) {
const rawValue = barcodes[0].rawValue;
if (rawValue && CIP_REGEX.test(rawValue)) {
found = true;
stopCamera();
playBeep();
setScannedCip(rawValue);
setPhase('prescriptions');
fetchPrescriptions(rawValue);
return;
}
}
} catch { /* detector not ready */ }
if (!found) {
rafRef.current = requestAnimationFrame(scanFrame);
}
}
scanFrame();
} catch (err) {
stopCamera();
if (err.name === 'NotAllowedError') {
setErrorMsg('Camera permission denied. Please allow camera access and try again.');
} else if (err.name === 'NotFoundError') {
setErrorMsg('No camera detected. Enter your CIP code manually.');
} else {
setErrorMsg(`Camera error: ${err.message || 'Unknown error'}`);
}
setPhase('error');
}
}
function handleStartScan() {
if (isNative) {
handleNativeScan();
} else {
handleWebScan();
}
}
function handleManualSubmit(e) {
e.preventDefault();
const cip = manualCip.trim();
if (!cip) {
setErrorMsg('Please enter a CIP code.');
setPhase('error');
return;
}
if (!CIP_REGEX.test(cip)) {
setErrorMsg('Invalid CIP format. Must be 16 alphanumeric characters.');
setPhase('error');
return;
}
playBeep();
setScannedCip(cip);
setPhase('prescriptions');
fetchPrescriptions(cip);
}
function handlePickPrescription(rx) {
stopCamera();
onSelectMedicine(rx.name);
}
function handleBack() {
stopCamera();
if (phase === 'prescriptions') {
setPhase('idle');
setPrescriptions([]);
setScannedCip(null);
} else {
onClose();
}
}
return (
<div className="scanner-overlay">
<div className="scanner-topbar">
<button className="scanner-back-btn" onClick={handleBack} aria-label="Back">
Back
</button>
<h2 className="scanner-title">
{phase === 'prescriptions' ? '✅ TSI Scanned' : '📷 Scan TSI Card'}
</h2>
<span />
</div>
{/* ── Idle / Error / Scanning state ────────────────── */}
{phase !== 'prescriptions' && (
<div className="scanner-content">
{phase === 'error' && (
<div className="scan-error-panel">
<span className="scan-error-icon">🚫</span>
<p className="scan-error-msg">{errorMsg}</p>
<button className="scan-retry-btn" onClick={() => { setErrorMsg(''); setPhase('idle'); }}>
Try Again
</button>
</div>
)}
{phase === 'scanning' && !isNative && (
<div className="scanner-camera-container">
<video ref={videoRef} className="scanner-video" autoPlay muted playsInline />
<div className="scanner-frame">
<div className="corner tl" />
<div className="corner tr" />
<div className="corner bl" />
<div className="corner br" />
</div>
<p className="scanner-hint">Point at barcode hold steady</p>
</div>
)}
{phase === 'scanning' && isNative && (
<div className="scan-active-panel">
<div className="scanner-spinner" />
<p>Opening camera Point at barcode</p>
</div>
)}
{phase === 'idle' && (
<button className="scan-start-btn" onClick={handleStartScan}>
<span className="scan-start-icon">📷</span>
{isNative ? 'Start Scanning' : 'Open Camera to Scan'}
</button>
)}
<form className="manual-cip-section" onSubmit={handleManualSubmit}>
<label className="cip-label" htmlFor="cip-input">Or enter CIP manually</label>
<div className="cip-input-group">
<input
id="cip-input"
className="cip-input"
type="text"
placeholder="Enter CIP code manually"
value={manualCip}
onChange={(e) => setManualCip(e.target.value)}
maxLength={16}
autoComplete="off"
spellCheck={false}
/>
<button type="submit" className="cip-submit-btn">Submit</button>
</div>
</form>
</div>
)}
{/* ── Prescriptions result panel ───────────────────── */}
{phase === 'prescriptions' && (
<div className="scanner-prescriptions">
<div className="scanned-card-info">
<span className="scanned-icon">💳</span>
<div>
<p className="scanned-cip">{scannedCip}</p>
</div>
</div>
<h3 className="rx-heading">Active Prescriptions</h3>
<p className="rx-subheading">Tap a medicine to check availability at nearby pharmacies.</p>
{loadingPrescriptions && (
<div className="rx-loading">
<div className="scanner-spinner" />
<p>Loading prescriptions</p>
</div>
)}
{!loadingPrescriptions && prescriptions.length === 0 && (
<p className="rx-empty">No active prescriptions found for this card.</p>
)}
<ul className="rx-list">
{prescriptions.map((rx, i) => (
<li key={i}>
<button className="rx-item" onClick={() => handlePickPrescription(rx)}>
<div className="rx-item-info">
<span className="rx-name">{rx.name}</span>
<span className="rx-detail">{rx.dosage} · {rx.form}</span>
{rx.active_ingredient && (
<span className="rx-ingredient">{rx.active_ingredient}</span>
)}
</div>
<span className="rx-arrow"></span>
</button>
</li>
))}
</ul>
<button className="scan-again-btn" onClick={() => {
stopCamera();
setPhase('idle');
setPrescriptions([]);
setScannedCip(null);
}}>
🔄 Scan Again
</button>
</div>
)}
</div>
);
}
export default ScannerView;
+3
View File
@@ -49,7 +49,10 @@
<string>FarmaFinder uses your location to sort nearby pharmacies by distance.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>FarmaFinder uses your location to sort nearby pharmacies by distance.</string>
<key>NSCameraUsageDescription</key>
<string>FarmaFinder uses the camera to scan your TSI health card barcode and QR code.</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
+1 -1
View File
@@ -1,6 +1,6 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
platform :ios, '15.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
+190 -313
View File
@@ -8,67 +8,78 @@
"name": "farma-finder",
"version": "1.0.0",
"dependencies": {
"@capacitor/android": "^6.2.1",
"@capacitor/app": "^6.0.3",
"@capacitor/core": "^6.2.1",
"@capacitor/ios": "^6.2.1",
"@capacitor/splash-screen": "^6.0.4",
"@capacitor/status-bar": "^6.0.3"
"@capacitor/android": "^8.4.1",
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.1",
"@capacitor/ios": "^8.4.1",
"@capacitor/splash-screen": "^8.0.1",
"@capacitor/status-bar": "^8.0.2"
},
"devDependencies": {
"@capacitor/cli": "^6.2.1",
"@capacitor/cli": "^8.4.1",
"npm-run-all": "^4.1.5"
}
},
"node_modules/@capacitor/android": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-6.2.1.tgz",
"integrity": "sha512-8gd4CIiQO5LAIlPIfd5mCuodBRxMMdZZEdj8qG8m+dQ1sQ2xyemVpzHmRK8qSCHorsBUCg3D62j2cp6bEBAkdw==",
"license": "MIT",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.1.tgz",
"integrity": "sha512-igtDCJ7QQn0P2qHFD9p4KXaa6V1b2PRNt+MxjVwtjTm/BJvqmiazOJq6rPjwFSZnfHm6iFoZk8TfzHd44pyBGw==",
"peerDependencies": {
"@capacitor/core": "^6.2.0"
"@capacitor/core": "^8.4.0"
}
},
"node_modules/@capacitor/app": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/app/-/app-6.0.3.tgz",
"integrity": "sha512-4gFUCbcVz0N/YYN32OBFerocWXslIv3Nc90gDiRsBkJc0plwK6kIUT6PKa5WtW2kfhteUeCVXQbvArH2fH+0Ug==",
"license": "MIT",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz",
"integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/cli": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-6.2.1.tgz",
"integrity": "sha512-JKl0FpFge8PgQNInw12kcKieQ4BmOyazQ4JGJOfEpVXlgrX1yPhSZTPjngupzTCiK3I7q7iGG5kjun0fDqgSCA==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.4.1.tgz",
"integrity": "sha512-t7F2s7fFHCq113xgrggrmK6ctV0/8E5YfLNVLfPHp4GCTDO+tly9fZvWPf2/sOI8lMm18dLT43qbXLRTz/OZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ionic/cli-framework-output": "^2.2.5",
"@ionic/utils-fs": "^3.1.6",
"@ionic/utils-subprocess": "2.1.11",
"@ionic/utils-terminal": "^2.3.3",
"commander": "^9.3.0",
"debug": "^4.3.4",
"@ionic/cli-framework-output": "^2.2.8",
"@ionic/utils-subprocess": "^3.0.1",
"@ionic/utils-terminal": "^2.3.5",
"commander": "^12.1.0",
"debug": "^4.4.0",
"env-paths": "^2.2.0",
"kleur": "^4.1.4",
"native-run": "^2.0.0",
"fs-extra": "^11.2.0",
"kleur": "^4.1.5",
"native-run": "^2.0.3",
"open": "^8.4.0",
"plist": "^3.0.5",
"plist": "^3.1.0",
"prompts": "^2.4.2",
"rimraf": "^4.4.1",
"semver": "^7.3.7",
"tar": "^6.1.11",
"tslib": "^2.4.0",
"xml2js": "^0.5.0"
"rimraf": "^6.0.1",
"semver": "^7.6.3",
"tar": "^7.5.3",
"tslib": "^2.8.1",
"xml2js": "^0.6.2"
},
"bin": {
"cap": "bin/capacitor",
"capacitor": "bin/capacitor"
},
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"
}
},
"node_modules/@capacitor/cli/node_modules/fs-extra": {
"version": "11.3.5",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
"integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/@capacitor/cli/node_modules/semver": {
@@ -85,39 +96,35 @@
}
},
"node_modules/@capacitor/core": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
"license": "MIT",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.1.tgz",
"integrity": "sha512-xqhOGLbTAYeOWK+IDUNSjQJAPapQjRHrIcgk9PYp52or9zFTaaMko31uNi16N6W+CRJ8VrRram6fOYILkBG2Hg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/ios": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
"integrity": "sha512-tbMlQdQjxe1wyaBvYVU1yTojKJjgluZQsJkALuJxv/6F8QTw5b6vd7X785O/O7cMpIAZfUWo/vtAHzFkRV+kXw==",
"license": "MIT",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.4.1.tgz",
"integrity": "sha512-EgcAk7NYheHMTyP3CUrA65qKs4D2UEAKgw44HI6Uk3dTw6KjLQkdLOQWvbeRncHaZ2gklfOojUoc5DlSw2lhYg==",
"peerDependencies": {
"@capacitor/core": "^6.2.0"
"@capacitor/core": "^8.4.0"
}
},
"node_modules/@capacitor/splash-screen": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz",
"integrity": "sha512-uJXR+28cdaie7zIIUBvgkWgHim6Gr1itJym9voIMTmrjXkOaPtejwxYJsdQWPJz9zgGnSbXuC1mNNibLgv3OpQ==",
"license": "MIT",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.1.tgz",
"integrity": "sha512-c/ew/Z3eA7z8l06WoRAtzVF16VwYYrExmHmfGq1Cg675pVzaC/yuucB8/1xG1vhEfnW4fZ1KhSf/kzR1RiVYgg==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.3.tgz",
"integrity": "sha512-nFlgSmtx6Zwaw0tEvZgQsWHBeOfWWB/AvEoCApopLT4mHkBVoSrwkLvy2PjZs5wxCbsmqvQczr3XCyTwaDZVQg==",
"license": "MIT",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
"integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@ionic/cli-framework-output": {
@@ -136,17 +143,16 @@
}
},
"node_modules/@ionic/utils-array": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz",
"integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
"integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.0.0",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
"node": ">=16.0.0"
}
},
"node_modules/@ionic/utils-fs": {
@@ -166,127 +172,65 @@
}
},
"node_modules/@ionic/utils-object": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz",
"integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz",
"integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.0.0",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
"node": ">=16.0.0"
}
},
"node_modules/@ionic/utils-process": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz",
"integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==",
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz",
"integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ionic/utils-object": "2.1.5",
"@ionic/utils-terminal": "2.3.3",
"@ionic/utils-object": "2.1.6",
"@ionic/utils-terminal": "2.3.5",
"debug": "^4.0.0",
"signal-exit": "^3.0.3",
"tree-kill": "^1.2.2",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
}
},
"node_modules/@ionic/utils-process/node_modules/@ionic/utils-terminal": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz",
"integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/slice-ansi": "^4.0.0",
"debug": "^4.0.0",
"signal-exit": "^3.0.3",
"slice-ansi": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"tslib": "^2.0.1",
"untildify": "^4.0.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10.3.0"
"node": ">=16.0.0"
}
},
"node_modules/@ionic/utils-stream": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz",
"integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz",
"integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.0.0",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
"node": ">=16.0.0"
}
},
"node_modules/@ionic/utils-subprocess": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz",
"integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz",
"integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ionic/utils-array": "2.1.5",
"@ionic/utils-fs": "3.1.6",
"@ionic/utils-process": "2.1.10",
"@ionic/utils-stream": "3.1.5",
"@ionic/utils-terminal": "2.3.3",
"@ionic/utils-array": "2.1.6",
"@ionic/utils-fs": "3.1.7",
"@ionic/utils-process": "2.1.12",
"@ionic/utils-stream": "3.1.7",
"@ionic/utils-terminal": "2.3.5",
"cross-spawn": "^7.0.3",
"debug": "^4.0.0",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
}
},
"node_modules/@ionic/utils-subprocess/node_modules/@ionic/utils-fs": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz",
"integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/fs-extra": "^8.0.0",
"debug": "^4.0.0",
"fs-extra": "^9.0.0",
"tslib": "^2.0.1"
},
"engines": {
"node": ">=10.3.0"
}
},
"node_modules/@ionic/utils-subprocess/node_modules/@ionic/utils-terminal": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz",
"integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/slice-ansi": "^4.0.0",
"debug": "^4.0.0",
"signal-exit": "^3.0.3",
"slice-ansi": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"tslib": "^2.0.1",
"untildify": "^4.0.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10.3.0"
"node": ">=16.0.0"
}
},
"node_modules/@ionic/utils-subprocess/node_modules/cross-spawn": {
@@ -294,7 +238,6 @@
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -309,7 +252,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -319,7 +261,6 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -332,7 +273,6 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -342,7 +282,6 @@
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
@@ -374,6 +313,18 @@
"node": ">=16.0.0"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@types/fs-extra": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
@@ -657,13 +608,12 @@
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
"node": ">=18"
}
},
"node_modules/color-convert": {
@@ -684,13 +634,12 @@
"license": "MIT"
},
"node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || >=14"
"node": ">=18"
}
},
"node_modules/concat-map": {
@@ -1078,39 +1027,6 @@
"node": ">=10"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1220,46 +1136,53 @@
}
},
"node_modules/glob": {
"version": "9.3.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
"integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"minimatch": "^8.0.2",
"minipass": "^4.2.4",
"path-scurry": "^1.6.1"
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz",
"integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^5.0.5"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -1901,11 +1824,13 @@
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"dev": true,
"license": "ISC"
"engines": {
"node": "20 || >=22"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
@@ -1940,53 +1865,24 @@
}
},
"node_modules/minipass": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
"integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=8"
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
"node": ">= 18"
}
},
"node_modules/ms": {
@@ -2148,6 +2044,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -2180,32 +2082,21 @@
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": ">=16 || 14 >=14.18"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@@ -2394,19 +2285,19 @@
}
},
"node_modules/rimraf": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz",
"integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
"integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^9.2.0"
"glob": "^13.0.3",
"package-json-from-dist": "^1.0.1"
},
"bin": {
"rimraf": "dist/cjs/src/bin.js"
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -2947,32 +2838,19 @@
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"version": "7.5.17",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.17.tgz",
"integrity": "sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g==",
"dev": true,
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=8"
"node": ">=18"
}
},
"node_modules/through2": {
@@ -2990,7 +2868,6 @@
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
@@ -3300,11 +3177,10 @@
"license": "MIT"
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
@@ -3318,7 +3194,6 @@
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0"
}
@@ -3334,11 +3209,13 @@
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"dev": true,
"license": "ISC"
"engines": {
"node": ">=18"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
+7 -7
View File
@@ -20,15 +20,15 @@
"cap:run:ios": "npm run build:web && cap run ios"
},
"devDependencies": {
"@capacitor/cli": "^6.2.1",
"@capacitor/cli": "^8.4.1",
"npm-run-all": "^4.1.5"
},
"dependencies": {
"@capacitor/android": "^6.2.1",
"@capacitor/app": "^6.0.3",
"@capacitor/core": "^6.2.1",
"@capacitor/ios": "^6.2.1",
"@capacitor/splash-screen": "^6.0.4",
"@capacitor/status-bar": "^6.0.3"
"@capacitor/android": "^8.4.1",
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.4.1",
"@capacitor/ios": "^8.4.1",
"@capacitor/splash-screen": "^8.0.1",
"@capacitor/status-bar": "^8.0.2"
}
}
+217
View File
@@ -0,0 +1,217 @@
import fs from 'fs';
import path from 'path';
const today = '22 de junio de 2026';
const docs = [
{
outPath: path.resolve('PRECONVERSION_OEPM_FARMAFINDER.pdf'),
title: 'DOCUMENTO DE PRECONVERSION',
subtitle: 'Hoja de preparacion para expediente de patente',
pages: [
[
{ type: 'cover' },
],
[
{ type: 'heading', text: '1. Identificacion del expediente' },
{ type: 'paragraph', text: 'Solicitante: Antoni Nuñez Romeu' },
{ type: 'paragraph', text: 'NIF/NIE/DNI: 45858029P' },
{ type: 'paragraph', text: 'Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221' },
{ type: 'paragraph', text: 'Titulo de la invencion: Sistema y procedimiento implementados por ordenador para la identificacion de necesidades terapeuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacutico geolocalizado y generacion de acciones de suministro.' },
{ type: 'heading', text: '2. Objeto de este documento' },
{ type: 'paragraph', text: 'Este documento se emite como hoja interna de preparacion y control del expediente antes de su presentacion ante la Oficina Española de Patentes y Marcas (OEPM). Su finalidad es reunir en un unico soporte los datos de identificacion, la referencia del objeto tecnico y la declaracion de que la documentacion principal del expediente ha sido preparada para su presentacion.' },
{ type: 'heading', text: '3. Contenido del expediente' },
{ type: 'bullet', text: 'Solicitud de patente.' },
{ type: 'bullet', text: 'Memoria descriptiva.' },
{ type: 'bullet', text: 'Reivindicaciones.' },
{ type: 'bullet', text: 'Resumen.' },
{ type: 'bullet', text: 'Anexo de dibujos.' },
{ type: 'bullet', text: 'Solicitud de reduccion de tasas para persona fisica.' },
{ type: 'heading', text: '4. Declaracion' },
{ type: 'paragraph', text: 'El solicitante declara que la documentacion aportada se corresponde con una unica invencion, que la descripcion y las reivindicaciones han sido redactadas de forma coherente y que el material adjunto se presenta a efectos de tramitacion ante la OEPM.' },
{ type: 'heading', text: '5. Firma' },
{ type: 'paragraph', text: `En Terrassa, a ${today}.` },
{ type: 'paragraph', text: 'Fdo.: Antoni Nuñez Romeu' },
{ type: 'paragraph', text: 'DNI/NIF: 45858029P' },
{ type: 'paragraph', text: 'Documento de apoyo interno. Revisar con agente de propiedad industrial antes de presentar.' },
],
],
},
{
outPath: path.resolve('REDUCCION_TASAS_OEPM_PERSONA_FISICA.pdf'),
title: 'SOLICITUD DE REDUCCION DE TASAS',
subtitle: 'Persona fisica',
pages: [
[
{ type: 'cover' },
],
[
{ type: 'heading', text: '1. Datos del solicitante' },
{ type: 'paragraph', text: 'Nombre y apellidos: Antoni Nuñez Romeu' },
{ type: 'paragraph', text: 'DNI/NIF: 45858029P' },
{ type: 'paragraph', text: 'Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221' },
{ type: 'heading', text: '2. Expediente al que se refiere' },
{ type: 'paragraph', text: 'Titulo de la invencion: Sistema y procedimiento implementados por ordenador para la identificacion de necesidades terapeuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacutico geolocalizado y generacion de acciones de suministro.' },
{ type: 'heading', text: '3. Solicitud' },
{ type: 'paragraph', text: 'Por medio del presente escrito, el solicitante, en su condicion de persona fisica, solicita que se le aplique la reduccion de tasas que resulte procedente conforme a la normativa vigente de la Oficina Española de Patentes y Marcas (OEPM) para personas fisicas.' },
{ type: 'paragraph', text: 'A tal efecto, manifiesta que la solicitud de patente se presenta a su nombre como persona fisica y pide que la oficina tramite esta peticion de conformidad con los requisitos y porcentajes de reduccion establecidos en la normativa aplicable al momento de la presentacion.' },
{ type: 'heading', text: '4. Declaracion responsable' },
{ type: 'paragraph', text: 'El solicitante declara bajo su responsabilidad que los datos consignados en este documento son ciertos y que aportara, en su caso, la documentacion adicional que la OEPM pudiera requerir para acreditar la condicion de persona fisica y la procedencia de la reduccion solicitada.' },
{ type: 'heading', text: '5. Firma' },
{ type: 'paragraph', text: `En Terrassa, a ${today}.` },
{ type: 'paragraph', text: 'Fdo.: Antoni Nuñez Romeu' },
{ type: 'paragraph', text: 'DNI/NIF: 45858029P' },
{ type: 'paragraph', text: 'Nota: revisar con agente o con la OEPM el formulario exacto y los requisitos vigentes antes de presentar.' },
],
],
},
];
const PAGE_W = 595.28;
const PAGE_H = 841.89;
const MARGIN_L = 56;
const MARGIN_R = 56;
const MARGIN_T = 58;
const MARGIN_B = 52;
const CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R;
function esc(s) {
return String(s).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
function wrapText(text, maxChars) {
const words = String(text).split(/\s+/);
const lines = [];
let line = '';
for (const word of words) {
const candidate = line ? `${line} ${word}` : word;
if (candidate.length > maxChars && line) {
lines.push(line);
line = word;
} else {
line = candidate;
}
}
if (line) lines.push(line);
return lines;
}
function buildPdf({ title, subtitle, pages, outPath }) {
const objects = [''];
const pagesObjNums = [];
const contentObjNums = [];
function addObject(bodyText) {
objects.push(bodyText);
return objects.length - 1;
}
const f1 = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>');
const f2 = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>');
const f3 = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>');
function renderPage(pageNo, items, isCover = false) {
const ops = [];
let y = PAGE_H - MARGIN_T;
let numbering = 1;
const write = (txt, x, size, font = 'F1') => {
ops.push(`BT /${font} ${size} Tf ${x} ${y} Td (${esc(txt)}) Tj ET`);
y -= size * 1.28;
};
const drawLine = () => {
y -= 4;
ops.push(`0.2 w ${MARGIN_L} ${y} m ${PAGE_W - MARGIN_R} ${y} l S`);
y -= 10;
};
if (isCover) {
write(title, MARGIN_L + 70, 18, 'F2');
y -= 6;
write('Oficina Española de Patentes y Marcas (OEPM)', MARGIN_L + 55, 12, 'F1');
y -= 16;
write(subtitle, MARGIN_L + 80, 12, 'F3');
y -= 12;
ops.push(`0.8 w ${MARGIN_L} ${y - 66} ${CONTENT_W} 66 re S`);
write('Solicitante: Antoni Nuñez Romeu', MARGIN_L + 10, 11, 'F1');
write('DNI/NIF: 45858029P', MARGIN_L + 10, 11, 'F1');
write('Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221', MARGIN_L + 10, 11, 'F1');
write('Inventor: Antoni Nuñez Romeu', MARGIN_L + 10, 11, 'F1');
y -= 8;
write('Documento de apoyo para expediente de patente', MARGIN_L + 70, 10, 'F3');
return ops.join('\n');
}
for (const item of items) {
if (item.type === 'heading') {
write(item.text, MARGIN_L, 13, 'F2');
drawLine();
continue;
}
if (item.type === 'paragraph') {
const lines = wrapText(item.text, 82);
for (const line of lines) write(line, MARGIN_L, 11.2, 'F1');
y -= 1;
continue;
}
if (item.type === 'bullet') {
const lines = wrapText(item.text, 78);
let first = true;
for (const line of lines) {
write((first ? '• ' : ' ') + line, MARGIN_L + 8, 11.1, 'F1');
first = false;
}
continue;
}
if (item.type === 'number') {
const lines = wrapText(item.text, 76);
let first = true;
for (const line of lines) {
write((first ? `${numbering}. ` : ' ') + line, MARGIN_L + 8, 11.1, 'F1');
first = false;
}
numbering += 1;
continue;
}
}
write(`Pagina ${pageNo}`, PAGE_W - 100, 9, 'F1');
return ops.join('\n');
}
const pageContents = pages.map((items, idx) => renderPage(idx + 1, items, idx === 0 && items.length === 1 && items[0].type === 'cover'));
for (const content of pageContents) {
const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`;
contentObjNums.push(addObject(stream));
}
const pagesTree = addObject('');
for (let i = 0; i < pageContents.length; i++) {
const pageObj = addObject(`<< /Type /Page /Parent ${pagesTree} 0 R /MediaBox [0 0 ${PAGE_W} ${PAGE_H}] /Resources << /Font << /F1 ${f1} 0 R /F2 ${f2} 0 R /F3 ${f3} 0 R >> >> /Contents ${contentObjNums[i]} 0 R >>`);
pagesObjNums.push(pageObj);
}
objects[pagesTree] = `<< /Type /Pages /Kids [${pagesObjNums.map(n => `${n} 0 R`).join(' ')}] /Count ${pagesObjNums.length} >>`;
const catalog = addObject(`<< /Type /Catalog /Pages ${pagesTree} 0 R >>`);
let pdf = '%PDF-1.4\n';
const offsets = [0];
for (let i = 1; i < objects.length; i++) {
offsets[i] = Buffer.byteLength(pdf, 'latin1');
pdf += `${i} 0 obj\n${objects[i]}\nendobj\n`;
}
const xref = Buffer.byteLength(pdf, 'latin1');
pdf += `xref\n0 ${objects.length}\n`;
pdf += '0000000000 65535 f \n';
for (let i = 1; i < objects.length; i++) {
pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
}
pdf += `trailer << /Size ${objects.length} /Root ${catalog} 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
fs.writeFileSync(outPath, Buffer.from(pdf, 'latin1'));
}
for (const doc of docs) {
buildPdf(doc);
console.log(`Wrote ${doc.outPath}`);
}
+242
View File
@@ -0,0 +1,242 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
const OUT_DIR = process.cwd();
const packageDocs = [
{
outPath: path.resolve(OUT_DIR, 'SOLICITUD_Y_DIBUJOS_OEPM_FARMAFINDER.pdf'),
pages: [
{ kind: 'cover', title: 'ANEXO DE DIBUJOS E INSTANCIA', subtitle: 'Expediente de Patente para FarmaFinder' },
{ kind: 'instance' },
{ kind: 'diagram1' },
{ kind: 'diagram2' },
{ kind: 'diagram3' },
{ kind: 'diagram4' }
]
}
];
function esc(s) {
return String(s).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
function makePdf({ outPath, pages }) {
const PAGE_W = 595.28;
const PAGE_H = 841.89;
const MARGIN_L = 48;
const MARGIN_R = 48;
const CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R;
const objects = [''];
const contentNums = [];
const pageNums = [];
const add = (x) => (objects.push(x), objects.length - 1);
const f1 = add('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>');
const f2 = add('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>');
const f3 = add('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>');
const wrap = (text, max) => {
const words = String(text).split(/\s+/);
const lines = [];
let line = '';
for (const word of words) {
const cand = line ? `${line} ${word}` : word;
if (cand.length > max && line) {
lines.push(line);
line = word;
} else {
line = cand;
}
}
if (line) lines.push(line);
return lines;
};
function pageOps(page) {
const ops = [];
const drawText = (txt, x, y, size = 11, font = 'F1') => {
ops.push(`BT /${font} ${size} Tf ${x} ${y} Td (${esc(txt)}) Tj ET`);
};
const line = (x1, y1, x2, y2, w = 1) => {
ops.push(`${w} w ${x1} ${y1} m ${x2} ${y2} l S`);
};
const rect = (x, y, w, h, lw = 1) => {
ops.push(`${lw} w ${x} ${y} ${w} ${h} re S`);
};
const arrow = (x1, y1, x2, y2) => {
line(x1, y1, x2, y2, 1);
const dx = x2 - x1;
const dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const ux = dx / len;
const uy = dy / len;
const ax = x2 - ux * 10;
const ay = y2 - uy * 10;
const px = -uy;
const py = ux;
line(x2, y2, ax + px * 4, ay + py * 4, 1);
line(x2, y2, ax - px * 4, ay - py * 4, 1);
};
const box = (x, y, w, h, title, body) => {
rect(x, y, w, h, 1);
drawText(title, x + 8, y + h - 18, 11, 'F2');
const lines = wrap(body, Math.max(18, Math.floor(w / 5.5)));
let ty = y + h - 34;
for (const l of lines) {
drawText(l, x + 8, ty, 9.5, 'F1');
ty -= 12;
}
};
if (page.kind === 'cover') {
drawText(page.title, 160, 770, 18, 'F2');
drawText(page.subtitle, 170, 748, 12, 'F3');
rect(70, 560, 455, 120, 1);
drawText('Solicitante: Antoni Nuñez Romeu', 90, 640, 11, 'F1');
drawText('DNI/NIF: 45858029P', 90, 622, 11, 'F1');
drawText('Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221', 90, 604, 11, 'F1');
drawText('Documentos incluidos: instancia, memoria, reivindicaciones, resumen, dibujos, tasa reducida', 90, 586, 10, 'F1');
drawText('Expediente preparado para presentacion ante OEPM', 145, 500, 10, 'F3');
drawText('Pagina 1', 470, 24, 9, 'F1');
return ops.join('\n');
}
if (page.kind === 'instance') {
drawText('INSTANCIA DE SOLICITUD', 175, 780, 16, 'F2');
drawText('Oficina Espanola de Patentes y Marcas (OEPM)', 140, 760, 11, 'F1');
line(48, 748, 547, 748, 0.8);
drawText('Datos del solicitante', 48, 725, 12, 'F2');
drawText('Nombre: Antoni Nuñez Romeu', 56, 705, 11, 'F1');
drawText('DNI/NIF: 45858029P', 56, 688, 11, 'F1');
drawText('Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221', 56, 671, 11, 'F1');
drawText('Tipo de presentacion: Patente nacional', 56, 654, 11, 'F1');
drawText('Titulo: Sistema y procedimiento implementados por ordenador para la identificacion de necesidades terapeuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacutico geolocalizado y generacion de acciones de suministro.', 56, 628, 10.3, 'F1');
drawText('Documentacion anexa', 48, 590, 12, 'F2');
drawText('1. Memoria descriptiva', 60, 570, 10.8, 'F1');
drawText('2. Reivindicaciones', 60, 554, 10.8, 'F1');
drawText('3. Resumen', 60, 538, 10.8, 'F1');
drawText('4. Dibujos / laminas tecnicas', 60, 522, 10.8, 'F1');
drawText('5. Solicitud de reduccion de tasas (persona fisica)', 60, 506, 10.8, 'F1');
drawText('Declaracion: el solicitante afirma ser el inventor y unico titular de la solicitud presentada.', 48, 470, 10.5, 'F1');
drawText('Firma: Antoni Nuñez Romeu', 48, 442, 11, 'F1');
drawText('Fecha: 22 de junio de 2026', 48, 424, 11, 'F1');
drawText('Pagina 2', 470, 24, 9, 'F1');
return ops.join('\n');
}
if (page.kind === 'diagram1') {
drawText('Figura 1. Arquitectura general del sistema', 160, 790, 14, 'F2');
box(50, 620, 120, 70, '1 Usuario', 'Inicia busqueda y notificaciones');
box(220, 620, 120, 70, '2 Escaneo TSI', 'Captura de tarjeta y validacion');
box(390, 620, 120, 70, '3 Perfil', 'Autenticacion y ubicacion');
box(105, 470, 120, 70, '4 Stock', 'Farmacias y medicamentos');
box(275, 470, 120, 70, '5 Motor', 'Contraste, ordenacion y reglas');
box(445, 470, 90, 70, '6 Accion', 'Reserva / aviso / pedido');
arrow(170, 655, 220, 655);
arrow(340, 655, 390, 655);
arrow(280, 620, 165, 540);
arrow(340, 620, 335, 540);
arrow(445, 655, 490, 540);
arrow(225, 470, 275, 505);
arrow(395, 470, 445, 505);
drawText('Flujo: usuario -> captura -> verificacion -> consulta de stock -> accion', 110, 350, 10.5, 'F1');
drawText('Pagina 1 de dibujos', 450, 24, 9, 'F1');
return ops.join('\n');
}
if (page.kind === 'diagram2') {
drawText('Figura 2. Captura y validacion de la TSI', 175, 790, 14, 'F2');
box(70, 600, 140, 70, '1 Camara / lector', 'Entrada de imagen o codigo');
box(250, 600, 140, 70, '2 Extraccion', 'OCR / parseo / identificacion');
box(430, 600, 100, 70, '3 Validacion', 'Usuario autenticado');
box(160, 430, 160, 70, '4 Vinculo seguro', 'Asociacion a perfil y consentimiento');
box(360, 430, 160, 70, '5 Resultado', 'Necesidad terapeutica o referencia');
arrow(210, 635, 250, 635);
arrow(390, 635, 430, 635);
arrow(480, 600, 240, 500);
arrow(320, 600, 440, 500);
drawText('Pagina 2 de dibujos', 450, 24, 9, 'F1');
return ops.join('\n');
}
if (page.kind === 'diagram3') {
drawText('Figura 3. Contraste con stock farmaceutico', 160, 790, 14, 'F2');
box(50, 620, 130, 70, '1 Medicamento', 'Referencia obtenida del sistema');
box(220, 620, 130, 70, '2 Geolocalizacion', 'Distancia y prioridad');
box(390, 620, 130, 70, '3 Inventario', 'Stock, horario y precio');
box(135, 460, 140, 70, '4 Motor de ranking', 'Ordena por stock y proximidad');
box(330, 460, 140, 70, '5 Resultado', 'Lista de farmacias priorizada');
arrow(180, 655, 220, 655);
arrow(350, 655, 390, 655);
arrow(285, 620, 205, 530);
arrow(455, 620, 400, 530);
drawText('Pagina 3 de dibujos', 450, 24, 9, 'F1');
return ops.join('\n');
}
if (page.kind === 'diagram4') {
drawText('Figura 4. Flujo cuando no hay stock', 185, 790, 14, 'F2');
box(60, 620, 120, 70, '1 Sin stock', 'No existe disponibilidad inmediata');
box(230, 620, 120, 70, '2 Alerta', 'Aviso a farmacia o usuario');
box(400, 620, 120, 70, '3 Reserva', 'Lista de espera o reserva');
box(145, 450, 120, 70, '4 Pedido', 'Solicitud a proveedor o distribucion');
box(325, 450, 120, 70, '5 Alternativa', 'Otra farmacia con stock');
arrow(180, 655, 230, 655);
arrow(350, 655, 400, 655);
arrow(290, 620, 205, 520);
arrow(460, 620, 385, 520);
drawText('Pagina 4 de dibujos', 450, 24, 9, 'F1');
return ops.join('\n');
}
return ops.join('\n');
}
const pageContents = pages.map((p) => pageOps(p));
for (const content of pageContents) {
contentNums.push(add(`<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`));
}
const pagesTree = add('');
for (let i = 0; i < contentNums.length; i++) {
pageNums.push(add(`<< /Type /Page /Parent ${pagesTree} 0 R /MediaBox [0 0 ${PAGE_W} ${PAGE_H}] /Resources << /Font << /F1 ${f1} 0 R /F2 ${f2} 0 R /F3 ${f3} 0 R >> >> /Contents ${contentNums[i]} 0 R >>`));
}
objects[pagesTree] = `<< /Type /Pages /Kids [${pageNums.map(n => `${n} 0 R`).join(' ')}] /Count ${pageNums.length} >>`;
const catalog = add(`<< /Type /Catalog /Pages ${pagesTree} 0 R >>`);
let pdf = '%PDF-1.4\n';
const offsets = [0];
for (let i = 1; i < objects.length; i++) {
offsets[i] = Buffer.byteLength(pdf, 'latin1');
pdf += `${i} 0 obj\n${objects[i]}\nendobj\n`;
}
const xref = Buffer.byteLength(pdf, 'latin1');
pdf += `xref\n0 ${objects.length}\n`;
pdf += '0000000000 65535 f \n';
for (let i = 1; i < objects.length; i++) pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
pdf += `trailer << /Size ${objects.length} /Root ${catalog} 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
fs.writeFileSync(outPath, Buffer.from(pdf, 'latin1'));
}
// Generate Drawings and Instance PDF
for (const doc of packageDocs) {
makePdf(doc);
console.log(`Wrote ${doc.outPath}`);
}
// Zip final package and source archive
try {
console.log('Packaging all generated PDFs into a ZIP archive...');
// Force removal of existing zip first if any to avoid duplication issues
if (fs.existsSync('oepm_patent_package_pdf.zip')) fs.unlinkSync('oepm_patent_package_pdf.zip');
execSync('zip -j oepm_patent_package_pdf.zip SOLICITUD_PATENTE_OEPM_FARMAFINDER.pdf PRECONVERSION_OEPM_FARMAFINDER.pdf REDUCCION_TASAS_OEPM_PERSONA_FISICA.pdf SOLICITUD_Y_DIBUJOS_OEPM_FARMAFINDER.pdf', { stdio: 'inherit' });
console.log('Created oepm_patent_package_pdf.zip successfully.');
console.log('Packaging pre-conversion source files into a ZIP archive...');
if (fs.existsSync('oepm_preconversion_archive.zip')) fs.unlinkSync('oepm_preconversion_archive.zip');
execSync('zip -r oepm_preconversion_archive.zip SOLICITUD_PATENTE_OEPM_FARMAFINDER.html SOLICITUD_PATENTE_OEPM_FARMAFINDER.md MEMORIA_TECNICA.md PLAN.md dibujos/', { stdio: 'inherit' });
console.log('Created oepm_preconversion_archive.zip successfully.');
} catch (err) {
console.error('Error creating zip archives:', err.message);
}
+343
View File
@@ -0,0 +1,343 @@
import fs from 'fs';
import path from 'path';
const outPath = path.resolve('SOLICITUD_PATENTE_OEPM_FARMAFINDER.pdf');
const pages = [
[
{ type: 'title', text: 'SOLICITUD DE PATENTE' },
{ type: 'center', text: 'Oficina Española de Patentes y Marcas (OEPM)' },
{ type: 'spacer', lines: 2 },
{
type: 'center',
text: 'Título de la invención:',
},
{
type: 'center',
text: 'Sistema y procedimiento implementados por ordenador para la identificación de necesidades',
},
{
type: 'center',
text: 'terapéuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacéutico',
},
{
type: 'center',
text: 'geolocalizado y generación de acciones de suministro.',
},
{ type: 'spacer', lines: 2 },
{ type: 'box', lines: [
'Solicitante: Antoni Nuñez Romeu',
'NIF/CIF: 45858029P',
'Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221',
'Inventor o inventores: Antoni Nuñez Romeu',
]},
{ type: 'spacer', lines: 2 },
{ type: 'centerSmall', text: 'Memoria descriptiva, reivindicaciones, resumen y referencia a dibujos' },
],
[
{ type: 'heading', text: '1. Campo técnico de la invención' },
{ type: 'paragraph', text: 'La invención se encuadra en los sistemas informáticos aplicados al sector sanitario y farmacéutico, en particular en soluciones de captura de identificadores sanitarios, comparación automatizada de disponibilidad de medicamentos y asistencia digital a la dispensación y suministro.' },
{ type: 'heading', text: '2. Antecedentes de la invención' },
{ type: 'paragraph', text: 'Las soluciones actuales de búsqueda de medicamentos y farmacias permiten localizar productos y mostrar stock aproximado, pero normalmente operan a partir de una búsqueda manual por nombre de medicamento o principio activo. También existen sistemas de geolocalización y de gestión de inventario, pero suelen estar desacoplados del contexto sanitario del usuario y no integran, en una única cadena técnica, la identificación mediante tarjeta sanitaria individual, la inferencia de necesidades terapéuticas y la comparación en tiempo real con disponibilidad farmacéutica local.' },
{ type: 'paragraph', text: 'Además, cuando no existe stock en la farmacia más próxima, el usuario debe iniciar manualmente nuevos procesos de búsqueda o desplazamiento, sin que el sistema proponga de forma automatizada acciones técnicas de reserva, pedido o derivación basadas en la disponibilidad real.' },
{ type: 'heading', text: '3. Problema técnico a resolver' },
{ type: 'paragraph', text: 'La invención resuelve el problema técnico de reducir el tiempo y la fricción entre la identificación de una necesidad terapéutica y la localización de una farmacia que disponga del medicamento adecuado, integrando:' },
{ type: 'bullet', text: 'lectura segura de identificadores sanitarios,' },
{ type: 'bullet', text: 'obtención de referencias de medicamento asociadas,' },
{ type: 'bullet', text: 'comparación automatizada con inventarios de farmacias,' },
{ type: 'bullet', text: 'priorización por proximidad y disponibilidad,' },
{ type: 'bullet', text: 'generación de acciones técnicas de suministro cuando no exista stock inmediato.' },
{ type: 'heading', text: '4. Resumen de la invención' },
{ type: 'paragraph', text: 'La invención propone un sistema y procedimiento implementados por ordenador que operan sobre un terminal móvil o dispositivo de lectura. El sistema captura la información de una tarjeta sanitaria individual mediante escaneo, valida la información mediante un módulo de verificación, asocia el resultado al perfil del usuario autenticado y obtiene una lista de medicamentos o necesidades terapéuticas relevantes. Posteriormente, un motor de contraste consulta un repositorio de stock de farmacias geolocalizadas y determina la coincidencia entre la necesidad detectada y la disponibilidad real en cada establecimiento. Si existe stock, el sistema prioriza las farmacias adecuadas y presenta una ruta de acceso o reserva. Si no existe stock, el sistema genera automáticamente una acción de aviso, pedido, reserva o derivación, conservando trazabilidad técnica del evento. La invención no se limita a una búsqueda manual, sino que integra captura, validación, georreferenciación, consulta de stock y respuesta automática en una única arquitectura técnica.' },
],
[
{ type: 'heading', text: '5. Breve descripción de los dibujos' },
{ type: 'paragraph', text: 'Figura 1. Diagrama de bloques general del sistema.' },
{ type: 'paragraph', text: 'Figura 2. Flujo de captura y validación del escaneo de la TSI.' },
{ type: 'paragraph', text: 'Figura 3. Flujo de contraste entre necesidad terapéutica y stock de farmacias.' },
{ type: 'paragraph', text: 'Figura 4. Flujo de acción cuando no existe stock disponible.' },
{ type: 'heading', text: '6. Descripción detallada de la invención' },
{ type: 'subheading', text: '6.1 Arquitectura general' },
{ type: 'paragraph', text: 'El sistema comprende al menos los siguientes módulos:' },
{ type: 'number', text: 'un módulo de captura de imagen o lectura óptica para escanear la tarjeta sanitaria individual;' },
{ type: 'number', text: 'un módulo de extracción de datos para identificar los campos relevantes de la tarjeta;' },
{ type: 'number', text: 'un módulo de verificación de identidad y autorización, vinculado al usuario autenticado;' },
{ type: 'number', text: 'un módulo de obtención de necesidades terapéuticas o medicamentos asociados;' },
{ type: 'number', text: 'un módulo de geolocalización para obtener la posición del usuario o una posición de referencia;' },
{ type: 'number', text: 'un módulo de consulta de inventario para recuperar stock, precio y horario de farmacias;' },
{ type: 'number', text: 'un módulo de contraste y priorización para ordenar farmacias según disponibilidad y cercanía;' },
{ type: 'number', text: 'un módulo de generación de acciones para crear reserva, aviso, pedido o derivación;' },
{ type: 'number', text: 'un módulo de trazabilidad y auditoría.' },
{ type: 'subheading', text: '6.2 Funcionamiento' },
{ type: 'paragraph', text: 'En una realización preferida, el usuario abre la aplicación y solicita el escaneo de su tarjeta sanitaria individual. El terminal captura la imagen o el código de la tarjeta y el módulo de extracción identifica los datos técnicos necesarios para enlazar el contexto sanitario del usuario con un conjunto de medicamentos o referencias de tratamiento.' },
{ type: 'paragraph', text: 'El sistema valida que la operación se ejecuta sobre un usuario autenticado y autorizado. A continuación, el motor de contraste consulta el inventario de farmacias cercanas, recuperando para cada farmacia al menos su ubicación, horario, stock y, opcionalmente, precio.' },
],
[
{ type: 'subheading', text: '6.2 Funcionamiento (continuación)' },
{ type: 'paragraph', text: 'El módulo de priorización calcula una orden de resultados en base a criterios técnicos tales como existencia de stock, nivel de stock, distancia geográfica, horario de apertura y capacidad de reserva o solicitud de pedido.' },
{ type: 'paragraph', text: 'Cuando existe disponibilidad, el sistema muestra una lista priorizada de farmacias y puede iniciar una reserva o una indicación de recogida. Cuando no existe disponibilidad, el sistema no se limita a informar de la falta de stock, sino que genera una acción técnica adicional, por ejemplo alerta a la farmacia seleccionada, solicitud de pedido a distribución o proveedor, derivación automática a otra farmacia con stock o registro de una lista de espera o interés de suministro.' },
{ type: 'subheading', text: '6.3 Tratamiento de datos y seguridad' },
{ type: 'paragraph', text: 'La invención prevé que la información derivada del escaneo de la TSI se trate con medidas de minimización de datos, control de acceso, cifrado en tránsito y registro de eventos. El sistema no expone innecesariamente los datos de la tarjeta sanitaria y solo conserva los elementos imprescindibles para la operación descrita.' },
{ type: 'subheading', text: '6.4 Integración con la plataforma FarmaFinder' },
{ type: 'paragraph', text: 'En una implementación concreta, el sistema puede integrarse con una plataforma que ya disponga de catálogo de farmacias, geolocalización, stock por medicamento, autenticación de usuario, notificaciones push e histórico de acciones y preferencias.' },
{ type: 'paragraph', text: 'La integración permite que el resultado del escaneo de la TSI no se limite a una consulta pasiva, sino que se convierta en un disparador de decisiones técnicas sobre disponibilidad y suministro.' },
{ type: 'heading', text: '7. Ejemplo de realización' },
{ type: 'number', text: 'El usuario inicia sesión en la aplicación.' },
{ type: 'number', text: 'El usuario escanea su TSI con la cámara del terminal.' },
{ type: 'number', text: 'El sistema extrae el identificador necesario y lo vincula al perfil autorizado.' },
{ type: 'number', text: 'El motor terapéutico asocia una necesidad de medicamento o tratamiento.' },
],
[
{ type: 'heading', text: '7. Ejemplo de realización (continuación)' },
{ type: 'number', text: 'El sistema consulta farmacias cercanas con stock del medicamento.' },
{ type: 'number', text: 'Si hay stock, ordena las farmacias por proximidad y disponibilidad.' },
{ type: 'number', text: 'Si no hay stock, genera una reserva, una alerta o una solicitud de pedido.' },
{ type: 'heading', text: '8. Reivindicaciones' },
{ type: 'claim', text: 'Reivindicación 1. Sistema implementado por ordenador para identificar una necesidad terapéutica a partir del escaneo de una tarjeta sanitaria individual, caracterizado porque comprende medios de captura para obtener datos de la tarjeta sanitaria individual; medios de verificación para asociar dichos datos a un usuario autenticado; medios de obtención de una referencia de medicamento o necesidad terapéutica; medios de consulta de inventario para recuperar stock de farmacias geolocalizadas; medios de contraste para comparar la referencia de medicamento o necesidad terapéutica con el stock disponible; y medios de generación de acciones para producir una reserva, aviso, pedido o derivación cuando el stock disponible sea insuficiente.' },
{ type: 'claim', text: 'Reivindicación 2. Sistema según la reivindicación 1, en el que los medios de consulta de inventario ordenan las farmacias según una combinación de stock disponible, proximidad geográfica y horario de apertura.' },
{ type: 'claim', text: 'Reivindicación 3. Sistema según cualquiera de las reivindicaciones anteriores, en el que los datos de la tarjeta sanitaria individual son tratados mediante un módulo de minimización y cifrado para limitar su almacenamiento a los campos técnicamente necesarios.' },
{ type: 'claim', text: 'Reivindicación 4. Procedimiento implementado por ordenador para asistir en la localización y suministro de medicamentos, caracterizado porque comprende escanear una tarjeta sanitaria individual; verificar la autorización del usuario; derivar una necesidad terapéutica o referencia de medicamento; consultar stock de farmacias geolocalizadas; comparar la necesidad con el stock disponible; y generar una acción técnica de reserva, aviso, pedido o derivación cuando no exista disponibilidad suficiente.' },
{ type: 'claim', text: 'Reivindicación 5. Dispositivo móvil, terminal o sistema de lectura que comprende una cámara o lector óptico y que está configurado para ejecutar el sistema de cualquiera de las reivindicaciones anteriores.' },
],
[
{ type: 'heading', text: '9. Aplicación industrial' },
{ type: 'paragraph', text: 'La invención es susceptible de aplicación industrial en plataformas de asistencia farmacéutica, sistemas de gestión de inventario, servicios de reserva o pedido de medicamentos y terminales de consulta sanitaria.' },
{ type: 'heading', text: '10. Resumen' },
{ type: 'paragraph', text: 'La presente invención se refiere a un sistema y procedimiento implementados por ordenador que permiten, a partir del escaneo de una tarjeta sanitaria individual mediante un terminal móvil o dispositivo equivalente, asociar de forma segura un contexto de paciente a una necesidad terapéutica, obtener una referencia de medicamentos relevantes, comparar dicha necesidad con el stock disponible en farmacias geolocalizadas y generar, en caso de falta de disponibilidad, una acción de reserva, aviso, solicitud de pedido o derivación a una farmacia alternativa. El sistema integra captura de datos, verificación de identidad, geolocalización, consulta de disponibilidad, gestión de stock y generación de eventos de suministro en una única cadena técnica de procesamiento.' },
{ type: 'heading', text: '11. Anexo de dibujos' },
{ type: 'paragraph', text: 'Las figuras indicadas en la presente memoria tienen carácter esquemático y se incorporan a efectos ilustrativos de la arquitectura, del flujo de captura, del contraste de stock y de la generación de acciones. En una presentación definitiva, dichas figuras podrán acompañarse como láminas separadas numeradas de forma consecutiva.' },
{ type: 'heading', text: '12. Firma del solicitante' },
{ type: 'paragraph', text: 'En Terrassa, a 22 de junio de 2026.' },
{ type: 'paragraph', text: 'Fdo.: Antoni Nuñez Romeu' },
{ type: 'paragraph', text: 'DNI/NIF: 45858029P' },
{ type: 'heading', text: '13. Notas para presentación en OEPM' },
{ type: 'bullet', text: 'La solicitud debe presentarse como una única invención o un único concepto inventivo general.' },
{ type: 'bullet', text: 'La descripción, reivindicaciones y dibujos deben ser coherentes entre sí.' },
{ type: 'bullet', text: 'La protección en patente debe apoyarse en un efecto técnico concreto y no en una mera regla comercial o método abstracto.' },
{ type: 'bullet', text: 'Esta redacción evita formular una reivindicación independiente de "programa de ordenador como tal" y se centra en un sistema y un procedimiento implementados por ordenador con interacción técnica sobre captura, contraste de inventario y generación de acciones.' },
{ type: 'bullet', text: 'Antes de presentar la solicitud conviene una búsqueda de anterioridades y una revisión por agente de propiedad industrial.' },
],
];
const PAGE_W = 595.28;
const PAGE_H = 841.89;
const MARGIN_L = 56;
const MARGIN_R = 56;
const MARGIN_T = 58;
const MARGIN_B = 52;
const CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R;
const FONT = 'Times-Roman';
const FONT_BOLD = 'Times-Bold';
const FONT_ITALIC = 'Times-Italic';
function esc(s) {
return String(s)
.replace(/\\/g, '\\\\')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)');
}
function toLatin1(s) {
return Buffer.from(s, 'latin1').toString('latin1');
}
function wrapText(text, size) {
const avg = size * 0.5;
const max = Math.max(20, Math.floor(CONTENT_W / avg));
const words = String(text).split(/\s+/);
const lines = [];
let line = '';
for (const word of words) {
const candidate = line ? `${line} ${word}` : word;
if (candidate.length > max && line) {
lines.push(line);
line = word;
} else {
line = candidate;
}
}
if (line) lines.push(line);
return lines;
}
function makePdf() {
const objects = [''];
const pagesObjNums = [];
const contentObjNums = [];
const fontObjNums = { regular: 0, bold: 0, italic: 0 };
function addObject(body) {
objects.push(body);
return objects.length - 1;
}
fontObjNums.regular = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>');
fontObjNums.bold = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>');
fontObjNums.italic = addObject('<< /Type /Font /Subtype /Type1 /BaseFont /Times-Italic /Encoding /WinAnsiEncoding >>');
function pageContent(lines, pageNo) {
const ops = [];
let y = PAGE_H - MARGIN_T;
let numberCounter = 1;
const write = (txt, x, size, font = FONT) => {
const clean = toLatin1(txt);
ops.push(`BT /${font === FONT_BOLD ? 'F2' : font === FONT_ITALIC ? 'F3' : 'F1'} ${size} Tf ${x} ${y} Td (${esc(clean)}) Tj ET`);
y -= size * 1.28;
};
const separator = () => {
y -= 4;
ops.push(`0.2 w ${MARGIN_L} ${y} m ${PAGE_W - MARGIN_R} ${y} l S`);
y -= 10;
};
const box = (lines) => {
const lineH = 14;
const h = lines.length * lineH + 12;
ops.push(`0.8 w ${MARGIN_L} ${y - h + 8} ${CONTENT_W} ${h} re S`);
y -= 4;
for (const line of lines) {
write(line, MARGIN_L + 10, 11);
}
y -= 4;
};
const ensureSpace = (needed) => {
if (y - needed < MARGIN_B) {
return false;
}
return true;
};
if (pageNo === 1) {
write('SOLICITUD DE PATENTE', MARGIN_L + 85, 18, FONT_BOLD);
y -= 6;
write('Oficina Española de Patentes y Marcas (OEPM)', MARGIN_L + 55, 12, FONT);
y -= 14;
write('Título de la invención:', MARGIN_L + 120, 12, FONT_BOLD);
const titleLines = wrapText('Sistema y procedimiento implementados por ordenador para la identificación de necesidades terapéuticas mediante escaneo de tarjeta sanitaria individual, contraste con stock farmacéutico geolocalizado y generación de acciones de suministro.', 12);
for (const line of titleLines) write(line, MARGIN_L + 10, 12);
y -= 8;
box([
'Solicitante: Antoni Nuñez Romeu',
'NIF/CIF: 45858029P',
'Domicilio: C/ Font Vella 12, 1, Terrassa, Barcelona, 08221',
'Inventor o inventores: Antoni Nuñez Romeu',
]);
y -= 8;
write('Memoria descriptiva, reivindicaciones, resumen y referencia a dibujos', MARGIN_L + 68, 10, FONT_ITALIC);
} else {
const content = lines;
for (const item of content) {
if (item.type === 'heading') {
if (!ensureSpace(28)) break;
y -= 2;
write(item.text, MARGIN_L, 13, FONT_BOLD);
separator();
continue;
}
if (item.type === 'subheading') {
if (!ensureSpace(20)) break;
write(item.text, MARGIN_L, 11.5, FONT_BOLD);
continue;
}
if (item.type === 'paragraph') {
const wrapped = wrapText(item.text, 11.5);
const needed = wrapped.length * 15;
if (!ensureSpace(needed + 8)) break;
for (const line of wrapped) write(line, MARGIN_L, 11.5);
y -= 2;
continue;
}
if (item.type === 'bullet') {
const wrapped = wrapText(item.text, 11.2);
if (!ensureSpace(wrapped.length * 14 + 8)) break;
let first = true;
for (const line of wrapped) {
write((first ? '• ' : ' ') + line, MARGIN_L + 8, 11.2);
first = false;
}
continue;
}
if (item.type === 'number') {
const wrapped = wrapText(item.text, 11.2);
if (!ensureSpace(wrapped.length * 14 + 8)) break;
let first = true;
for (const line of wrapped) {
write((first ? `${numberCounter}. ` : ' ') + line, MARGIN_L + 8, 11.2);
first = false;
}
numberCounter += 1;
continue;
}
if (item.type === 'claim') {
const wrapped = wrapText(item.text, 11.2);
const needed = wrapped.length * 15 + 8;
if (!ensureSpace(needed)) break;
for (const line of wrapped) write(line, MARGIN_L, 11.2);
y -= 2;
continue;
}
if (item.type === 'center') {
const wrapped = wrapText(item.text, 12);
if (!ensureSpace(wrapped.length * 14)) break;
for (const line of wrapped) write(line, MARGIN_L + 70, 12);
continue;
}
if (item.type === 'centerSmall') {
const wrapped = wrapText(item.text, 10);
if (!ensureSpace(wrapped.length * 12)) break;
for (const line of wrapped) write(line, MARGIN_L + 78, 10);
continue;
}
if (item.type === 'title') {
continue;
}
if (item.type === 'spacer') {
y -= (item.lines || 1) * 12;
continue;
}
}
}
ops.push(`BT /F1 9 Tf ${PAGE_W - 100} ${24} Td (Pagina ${pageNo}) Tj ET`);
return ops.join('\n');
}
// Create page objects and content streams first
for (let i = 0; i < pages.length; i++) {
const content = pageContent(pages[i], i + 1);
const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`;
contentObjNums.push(addObject(stream));
}
const pagesTreeObj = addObject('');
for (let i = 0; i < pages.length; i++) {
const pageObj = addObject(`<< /Type /Page /Parent ${pagesTreeObj} 0 R /MediaBox [0 0 ${PAGE_W} ${PAGE_H}] /Resources << /Font << /F1 ${fontObjNums.regular} 0 R /F2 ${fontObjNums.bold} 0 R /F3 ${fontObjNums.italic} 0 R >> >> /Contents ${contentObjNums[i]} 0 R >>`);
pagesObjNums.push(pageObj);
}
objects[pagesTreeObj] = `<< /Type /Pages /Kids [${pagesObjNums.map(n => `${n} 0 R`).join(' ')}] /Count ${pagesObjNums.length} >>`;
const catalogObj = addObject(`<< /Type /Catalog /Pages ${pagesTreeObj} 0 R >>`);
let pdf = '%PDF-1.4\n';
const offsets = [0];
for (let i = 1; i < objects.length; i++) {
offsets[i] = Buffer.byteLength(pdf, 'latin1');
pdf += `${i} 0 obj\n${objects[i]}\nendobj\n`;
}
const xrefPos = Buffer.byteLength(pdf, 'latin1');
pdf += `xref\n0 ${objects.length}\n`;
pdf += '0000000000 65535 f \n';
for (let i = 1; i < objects.length; i++) {
pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
}
pdf += `trailer << /Size ${objects.length} /Root ${catalogObj} 0 R >>\nstartxref\n${xrefPos}\n%%EOF\n`;
fs.writeFileSync(outPath, Buffer.from(pdf, 'latin1'));
}
makePdf();
console.log(`Wrote ${outPath}`);