Logos & scanner changes
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "farma-finder-backend",
|
"name": "farma-clic-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "farma-finder-backend",
|
"name": "farma-clic-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -27,11 +27,13 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"multer": "^2.2.0",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"pino-http": "^10.3.0",
|
"pino-http": "^10.3.0",
|
||||||
"redis": "^4.6.0",
|
"redis": "^4.6.0",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
"tesseract.js": "^7.0.0",
|
||||||
"web-push": "^3.6.7"
|
"web-push": "^3.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3727,6 +3729,11 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-field": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
|
||||||
|
},
|
||||||
"node_modules/aproba": {
|
"node_modules/aproba": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||||
@@ -4020,6 +4027,11 @@
|
|||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bmp-js": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="
|
||||||
|
},
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "4.12.3",
|
"version": "4.12.3",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||||
@@ -4151,9 +4163,19 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -4426,6 +4448,20 @@
|
|||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/connect-pg-simple": {
|
"node_modules/connect-pg-simple": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
|
||||||
@@ -5682,6 +5718,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/idb-keyval": {
|
||||||
|
"version": "6.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.6.tgz",
|
||||||
|
"integrity": "sha512-FY64UEhw+5liMzMQ1R9Mw6AF0+wyBrg1CIA1z4CjI/EvT5ty/SvQcWZgd8s9sgaNhX10Y8UzScTh89tEAls5nA=="
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -5870,6 +5911,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-url": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -7056,6 +7102,24 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/multer": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"append-field": "^1.0.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"type-is": "^1.6.18"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/napi-build-utils": {
|
"node_modules/napi-build-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
@@ -7285,6 +7349,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/opencollective-postinstall": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
|
||||||
|
"bin": {
|
||||||
|
"opencollective-postinstall": "index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
@@ -7915,6 +7987,11 @@
|
|||||||
"@redis/time-series": "1.1.0"
|
"@redis/time-series": "1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -8477,6 +8554,14 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -8718,6 +8803,28 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tesseract.js": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bmp-js": "^0.1.0",
|
||||||
|
"idb-keyval": "^6.2.0",
|
||||||
|
"is-url": "^1.2.4",
|
||||||
|
"node-fetch": "^2.6.9",
|
||||||
|
"opencollective-postinstall": "^2.0.3",
|
||||||
|
"regenerator-runtime": "^0.13.3",
|
||||||
|
"tesseract.js-core": "^7.0.0",
|
||||||
|
"wasm-feature-detect": "^1.8.0",
|
||||||
|
"zlibjs": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tesseract.js-core": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw=="
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
@@ -8824,6 +8931,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
|
},
|
||||||
"node_modules/uid-safe": {
|
"node_modules/uid-safe": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
@@ -8964,6 +9076,11 @@
|
|||||||
"makeerror": "1.0.12"
|
"makeerror": "1.0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wasm-feature-detect": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ=="
|
||||||
|
},
|
||||||
"node_modules/web-push": {
|
"node_modules/web-push": {
|
||||||
"version": "3.6.7",
|
"version": "3.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||||
@@ -9168,6 +9285,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zlibjs": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,17 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"connect-sqlite3": "^0.9.16",
|
"connect-sqlite3": "^0.9.16",
|
||||||
"pg": "^8.13.0",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"multer": "^2.2.0",
|
||||||
|
"pg": "^8.13.0",
|
||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"pino-http": "^10.3.0",
|
"pino-http": "^10.3.0",
|
||||||
"redis": "^4.6.0",
|
"redis": "^4.6.0",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
"tesseract.js": "^7.0.0",
|
||||||
"web-push": "^3.6.7"
|
"web-push": "^3.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import rateLimit from 'express-rate-limit';
|
|||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import pinoHttp from 'pino-http';
|
import pinoHttp from 'pino-http';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { createWorker } from 'tesseract.js';
|
||||||
import { searchMedicines, getMedicineDetails } from './cima-service.js';
|
import { searchMedicines, getMedicineDetails } from './cima-service.js';
|
||||||
import { runFarmaciaWebhookImport, DEFAULT_FARMACIAS_WEBHOOK, importPharmaciesFromRows } from './farmacias-webhook-import.js';
|
import { runFarmaciaWebhookImport, DEFAULT_FARMACIAS_WEBHOOK, importPharmaciesFromRows } from './farmacias-webhook-import.js';
|
||||||
import { fetchPharmaciesExternal } from '../API/index.js';
|
import { fetchPharmaciesExternal } from '../API/index.js';
|
||||||
@@ -515,6 +517,57 @@ app.get('/api/tsi/:cip/prescriptions', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== TSI OCR: extract CIP from uploaded photo ==========
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB max
|
||||||
|
fileFilter: (_req, file, cb) => {
|
||||||
|
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||||
|
else cb(new Error('Only image files are accepted'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract CIP from OCR text — looks for a long alphanumeric token (16 chars typical for CIP)
|
||||||
|
function extractCip(text) {
|
||||||
|
const cleaned = text.replace(/\s+/g, '');
|
||||||
|
// Try exact 16-char alphanumeric match first
|
||||||
|
const exact = cleaned.match(/[A-Z0-9]{16}/i);
|
||||||
|
if (exact) return exact[0].toUpperCase();
|
||||||
|
// Fallback: 8–30 alphanumeric chars
|
||||||
|
const loose = cleaned.match(/[A-Z0-9]{8,30}/i);
|
||||||
|
if (loose) return loose[0].toUpperCase();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/api/tsi/ocr', upload.single('photo'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No image file provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await createWorker('spa+eng');
|
||||||
|
const { data } = await worker.recognize(req.file.buffer);
|
||||||
|
await worker.terminate();
|
||||||
|
|
||||||
|
console.log('[OCR] Raw text:', data.text);
|
||||||
|
const cip = extractCip(data.text);
|
||||||
|
console.log('[OCR] Extracted CIP:', cip);
|
||||||
|
|
||||||
|
if (!cip) {
|
||||||
|
return res.status(422).json({
|
||||||
|
error: 'No se pudo detectar un código CIP en la imagen',
|
||||||
|
ocrText: data.text.trim().slice(0, 500),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ cip });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TSI OCR error:', error);
|
||||||
|
res.status(500).json({ error: 'Error procesando la imagen' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========== AUTHENTICATION ROUTES ==========
|
// ========== AUTHENTICATION ROUTES ==========
|
||||||
|
|
||||||
// Login endpoint
|
// Login endpoint
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 34 KiB |
@@ -30,7 +30,7 @@
|
|||||||
--inverse-surface: #2e3132;
|
--inverse-surface: #2e3132;
|
||||||
--inverse-on-surface: #eff1f2;
|
--inverse-on-surface: #eff1f2;
|
||||||
--inverse-primary: #91d78a;
|
--inverse-primary: #91d78a;
|
||||||
--surface-tint: #2a6b2c;
|
--surface-tint: #8ccb8e;
|
||||||
--surface-dim: #d8dadb;
|
--surface-dim: #d8dadb;
|
||||||
--surface-bright: #f8fafb;
|
--surface-bright: #f8fafb;
|
||||||
--surface-variant: #e1e3e4;
|
--surface-variant: #e1e3e4;
|
||||||
|
|||||||
@@ -133,11 +133,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-brand-name {
|
.home-brand-name {
|
||||||
font-size: 2rem;
|
height: 2rem;
|
||||||
font-weight: 800;
|
width: auto;
|
||||||
color: var(--primary);
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 700px) {
|
@media (max-height: 700px) {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ function HomeView({ onScanClick, onSearchClick }) {
|
|||||||
<div className="home-view">
|
<div className="home-view">
|
||||||
<div className="home-hero">
|
<div className="home-hero">
|
||||||
<div className="home-logo-wrapper">
|
<div className="home-logo-wrapper">
|
||||||
<img src="/logo.png" alt="FarmaClic" className="home-logo" />
|
<img src="/farmaclic_logo.png" alt="FarmaClic" className="home-logo" />
|
||||||
<h1 className="home-brand-name">FarmaClic</h1>
|
<img src="/farmaclic_text.png" alt="FarmaClic" className="home-brand-name" />
|
||||||
</div>
|
</div>
|
||||||
<p className="home-desc">Encuentra tus medicamentos en farmacias cercanas</p>
|
<p className="home-desc">Encuentra tus medicamentos en farmacias cercanas</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -358,3 +358,87 @@
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Photo upload section */
|
||||||
|
.upload-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-option {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1.25rem 0.75rem;
|
||||||
|
background: var(--surface-container-lowest);
|
||||||
|
border: 2px solid var(--outline-variant);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface);
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-option:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--surface-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-option:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-option svg {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Photo preview */
|
||||||
|
.photo-preview-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview-container {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
max-height: 22rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
max-height: 22rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
const [loadingPrescriptions, setLoadingPrescriptions] = useState(false);
|
const [loadingPrescriptions, setLoadingPrescriptions] = useState(false);
|
||||||
const [errorMsg, setErrorMsg] = useState('');
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
|
|
||||||
|
// Photo upload state
|
||||||
|
const [previewUrl, setPreviewUrl] = useState(null);
|
||||||
|
const [ocrLoading, setOcrLoading] = useState(false);
|
||||||
|
const cameraInputRef = useRef(null);
|
||||||
|
const galleryInputRef = useRef(null);
|
||||||
|
|
||||||
const isNative = Capacitor.isNativePlatform();
|
const isNative = Capacitor.isNativePlatform();
|
||||||
|
|
||||||
const fetchPrescriptions = useCallback(async (cip) => {
|
const fetchPrescriptions = useCallback(async (cip) => {
|
||||||
@@ -237,6 +243,61 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
fetchPrescriptions(cip);
|
fetchPrescriptions(cip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Photo upload handlers
|
||||||
|
function handlePhotoSelect(e) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setErrorMsg('');
|
||||||
|
setPreviewUrl(URL.createObjectURL(file));
|
||||||
|
setPhase('photo-preview');
|
||||||
|
// Reset input so selecting the same file again triggers onChange
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePhotoOcr() {
|
||||||
|
if (!previewUrl) return;
|
||||||
|
setOcrLoading(true);
|
||||||
|
setErrorMsg('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(previewUrl);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('photo', blob, 'tsi.jpg');
|
||||||
|
|
||||||
|
const ocrRes = await fetch('/api/tsi/ocr', { method: 'POST', body: formData });
|
||||||
|
const data = await ocrRes.json();
|
||||||
|
|
||||||
|
if (!ocrRes.ok) {
|
||||||
|
setErrorMsg(data.error || 'No se pudo leer la imagen. Intenta con otra foto.');
|
||||||
|
setPhase('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cip = data.cip;
|
||||||
|
if (!CIP_REGEX.test(cip)) {
|
||||||
|
setErrorMsg(`CIP detectado "${cip}" no tiene formato válido. Introduce el código manualmente.`);
|
||||||
|
setPhase('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playBeep();
|
||||||
|
setScannedCip(cip);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setPhase('prescriptions');
|
||||||
|
fetchPrescriptions(cip);
|
||||||
|
} catch (err) {
|
||||||
|
setErrorMsg(`Error al procesar la imagen: ${err.message || 'Error desconocido'}`);
|
||||||
|
setPhase('error');
|
||||||
|
} finally {
|
||||||
|
setOcrLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePhotoDiscard() {
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setPhase('idle');
|
||||||
|
}
|
||||||
|
|
||||||
function handlePickPrescription(rx) {
|
function handlePickPrescription(rx) {
|
||||||
stopCamera();
|
stopCamera();
|
||||||
onSelectMedicine(rx.name);
|
onSelectMedicine(rx.name);
|
||||||
@@ -248,6 +309,9 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
setPhase('idle');
|
setPhase('idle');
|
||||||
setPrescriptions([]);
|
setPrescriptions([]);
|
||||||
setScannedCip(null);
|
setScannedCip(null);
|
||||||
|
} else if (phase === 'photo-preview') {
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setPhase('idle');
|
||||||
} else {
|
} else {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -285,6 +349,7 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{phase === 'idle' && (
|
{phase === 'idle' && (
|
||||||
|
<>
|
||||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||||
@@ -292,6 +357,43 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
</svg>
|
</svg>
|
||||||
Abrir cámara
|
Abrir cámara
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="upload-section">
|
||||||
|
<label className="upload-label">O sube una foto de tu TSI</label>
|
||||||
|
<div className="upload-row">
|
||||||
|
<button className="upload-option" onClick={() => cameraInputRef.current?.click()}>
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||||
|
<circle cx="12" cy="13" r="4" />
|
||||||
|
</svg>
|
||||||
|
<span>Hacer foto</span>
|
||||||
|
</button>
|
||||||
|
<button className="upload-option" onClick={() => galleryInputRef.current?.click()}>
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||||
|
<polyline points="21 15 16 10 5 21" />
|
||||||
|
</svg>
|
||||||
|
<span>Subir de galería</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={cameraInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
capture="environment"
|
||||||
|
onChange={handlePhotoSelect}
|
||||||
|
className="upload-hidden-input"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={galleryInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handlePhotoSelect}
|
||||||
|
className="upload-hidden-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{phase === 'scanning' && !isNative && (
|
{phase === 'scanning' && !isNative && (
|
||||||
@@ -314,6 +416,38 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{phase === 'photo-preview' && (
|
||||||
|
<div className="photo-preview-panel">
|
||||||
|
<div className="photo-preview-container">
|
||||||
|
<img src={previewUrl} alt="TSI capturada" className="photo-preview-img" />
|
||||||
|
</div>
|
||||||
|
{ocrLoading ? (
|
||||||
|
<div className="scanning-active">
|
||||||
|
<div className="scanner-spinner" />
|
||||||
|
<p>Procesando imagen…</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="photo-preview-actions">
|
||||||
|
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handlePhotoOcr}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="4 7 4 4 20 4 20 7" />
|
||||||
|
<line x1="9" y1="20" x2="15" y2="20" />
|
||||||
|
<line x1="12" y1="4" x2="12" y2="20" />
|
||||||
|
</svg>
|
||||||
|
Escanear imagen
|
||||||
|
</button>
|
||||||
|
<button className="scan-btn scan-btn--ghost" onClick={handlePhotoDiscard}>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
Descartar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form className="cip-form" onSubmit={handleManualSubmit}>
|
<form className="cip-form" onSubmit={handleManualSubmit}>
|
||||||
<label className="cip-label" htmlFor="cip-input">O introduce el código CIP manualmente</label>
|
<label className="cip-label" htmlFor="cip-input">O introduce el código CIP manualmente</label>
|
||||||
<div className="cip-row">
|
<div className="cip-row">
|
||||||
|
|||||||