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
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:
@@ -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
@@ -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.
|
||||
@@ -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,8 +2,8 @@
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+14
-14
@@ -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
@@ -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/
|
||||
|
||||
@@ -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
@@ -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 |
Generated
+90
-24
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+190
-313
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`);
|
||||
Reference in New Issue
Block a user