From d46f78bbd019cbd3eeb7f25b1fb0b34d7f9d3c4a Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 2 Sep 2026 18:23:05 +0530 Subject: [PATCH] Update frontend code --- .env | 3 + fix_search_height.cjs | 16 + make_search_wide.cjs | 17 + out.json | 0 package-lock.json | 510 ++++++++++++++++++++----- reduce_search_height.cjs | 21 + reduce_search_height_more.cjs | 24 ++ src/App.tsx | 11 +- src/components/AIChatWidget.tsx | 150 ++++++++ src/components/Header.tsx | 103 +++++ src/components/Sidebar.tsx | 12 +- src/components/VoiceAssistant.tsx | 29 +- src/index.css | 4 + src/pages/Dashboard.tsx | 45 ++- src/pages/History.tsx | 29 +- src/pages/Inventory.tsx | 103 ++--- src/pages/Patients.tsx | 22 +- src/pages/Pharmacy.tsx | 111 ++++-- src/pages/Prescriptions.tsx | 35 +- src/pages/Registers.tsx | 4 +- src/pages/StockMovements.tsx | 68 +++- src/pages/Suppliers.tsx | 4 +- src/pages/settings/ProfileSettings.tsx | 108 +++++- src/services/api.ts | 57 ++- vite.config.ts | 6 +- 25 files changed, 1213 insertions(+), 279 deletions(-) create mode 100644 .env create mode 100644 fix_search_height.cjs create mode 100644 make_search_wide.cjs create mode 100644 out.json create mode 100644 reduce_search_height.cjs create mode 100644 reduce_search_height_more.cjs create mode 100644 src/components/AIChatWidget.tsx create mode 100644 src/components/Header.tsx diff --git a/.env b/.env new file mode 100644 index 0000000..8f4d047 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:8080/v1 + +VITE_API_KEY=915e46784e798f21759e52bd2a9d4cc2 diff --git a/fix_search_height.cjs b/fix_search_height.cjs new file mode 100644 index 0000000..e9967ae --- /dev/null +++ b/fix_search_height.cjs @@ -0,0 +1,16 @@ +const fs = require('fs'); +const path = require('path'); +const dir = './src/pages'; +const files = fs.readdirSync(dir).filter(f => f.endsWith('.tsx')); + +for (const file of files) { + const filePath = path.join(dir, file); + let content = fs.readFileSync(filePath, 'utf8'); + + // Replace the style attribute for the input to include height: '64px' + // It currently looks like: style={{ paddingLeft: '60px', paddingRight: '16px', fontSize: '16px' }} + content = content.replace(/style=\{\{ paddingLeft: '60px', paddingRight: '16px', fontSize: '16px' \}\}/g, "style={{ height: '64px', paddingLeft: '60px', paddingRight: '16px', fontSize: '18px' }}"); + + fs.writeFileSync(filePath, content, 'utf8'); +} +console.log('Added explicit inline height 64px to search inputs.'); diff --git a/make_search_wide.cjs b/make_search_wide.cjs new file mode 100644 index 0000000..254792c --- /dev/null +++ b/make_search_wide.cjs @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = require('path'); +const dir = './src/pages'; +const files = fs.readdirSync(dir).filter(f => f.endsWith('.tsx')); + +for (const file of files) { + const filePath = path.join(dir, file); + let content = fs.readFileSync(filePath, 'utf8'); + + // Make the search bar wide by removing the max width restriction and adding flex-1 + content = content.replace(/className="relative w-full max-w-2xl group"/g, 'className="relative w-full flex-1 max-w-none group"'); + // Also check if some of them were modified differently + content = content.replace(/className="relative w-full max-w-md group"/g, 'className="relative w-full flex-1 max-w-none group"'); + + fs.writeFileSync(filePath, content, 'utf8'); +} +console.log('Made search bars full width.'); diff --git a/out.json b/out.json new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 7417123..12cd965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "ai_pharma_frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.3.3", "@tanstack/react-table": "^8.21.3", "axios": "^1.18.1", "lucide-react": "^1.26.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "recharts": "^3.10.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.3.3", @@ -46,7 +48,6 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -58,7 +59,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -69,7 +69,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -80,7 +79,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -91,7 +89,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -102,7 +99,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -112,14 +108,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -130,7 +124,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -149,7 +142,6 @@ "version": "0.139.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -478,6 +470,32 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -485,7 +503,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,7 +519,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -519,7 +535,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -536,7 +551,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -553,7 +567,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -570,7 +583,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -587,7 +599,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -604,7 +615,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -621,7 +631,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,7 +647,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -655,7 +663,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -672,7 +679,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -689,7 +695,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -705,7 +710,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -717,7 +721,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -731,7 +734,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -748,7 +750,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -762,14 +763,24 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -785,7 +796,6 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -818,7 +828,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -839,7 +848,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -860,7 +868,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -881,7 +888,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -902,7 +908,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -923,7 +928,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -944,7 +948,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -965,7 +968,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -986,7 +988,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1007,7 +1008,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1028,7 +1028,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1046,7 +1045,6 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -1073,7 +1071,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1090,7 +1087,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1107,7 +1103,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1124,7 +1119,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1141,7 +1135,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1158,7 +1151,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1175,7 +1167,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1192,7 +1183,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1209,7 +1199,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1234,7 +1223,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1256,7 +1244,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1273,7 +1260,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1297,6 +1283,20 @@ "tailwindcss": "4.3.3" } }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", @@ -1334,18 +1334,80 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1356,7 +1418,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1373,6 +1435,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", @@ -1548,6 +1616,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1577,9 +1654,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1597,6 +1795,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1610,7 +1814,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1641,7 +1844,6 @@ "version": "5.24.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -1696,6 +1898,17 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1706,11 +1919,16 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1778,7 +1996,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1851,7 +2068,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -1906,11 +2122,29 @@ "node": ">= 6" } }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -1920,7 +2154,6 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -1953,7 +2186,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1974,7 +2206,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1995,7 +2226,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2016,7 +2246,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2037,7 +2266,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2058,7 +2286,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2079,7 +2306,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2100,7 +2326,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2121,7 +2346,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2142,7 +2366,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2163,7 +2386,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2190,7 +2412,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -2236,7 +2457,6 @@ "version": "3.3.16", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, "funding": [ { "type": "github", @@ -2314,14 +2534,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -2335,7 +2553,6 @@ "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2400,6 +2617,37 @@ "react": "^19.2.8" } }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.18.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", @@ -2438,11 +2686,62 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.139.0", @@ -2488,7 +2787,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2498,14 +2796,12 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2515,11 +2811,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2536,7 +2837,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -2558,7 +2858,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2592,11 +2892,41 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { diff --git a/reduce_search_height.cjs b/reduce_search_height.cjs new file mode 100644 index 0000000..91ae8b8 --- /dev/null +++ b/reduce_search_height.cjs @@ -0,0 +1,21 @@ +const fs = require('fs'); +const path = require('path'); +const dir = './src/pages'; +const files = fs.readdirSync(dir).filter(f => f.endsWith('.tsx')); + +for (const file of files) { + const filePath = path.join(dir, file); + let content = fs.readFileSync(filePath, 'utf8'); + + // Reduce height from 64px to 54px, and adjust font size back to 16px + content = content.replace(/style=\{\{ height: '64px', paddingLeft: '60px', paddingRight: '16px', fontSize: '18px' \}\}/g, "style={{ height: '54px', paddingLeft: '56px', paddingRight: '16px', fontSize: '16px' }}"); + + // Also reduce the Search icon size from 28 to 24 + content = content.replace(/ f.endsWith('.tsx')); + +for (const file of files) { + const filePath = path.join(dir, file); + let content = fs.readFileSync(filePath, 'utf8'); + + // Reduce height from 54px to 46px, font size to 15px + content = content.replace(/style=\{\{ height: '54px', paddingLeft: '56px', paddingRight: '16px', fontSize: '16px' \}\}/g, "style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }}"); + + // Reduce the Search icon size from 24 to 20 + content = content.replace(/ -
- -
+
+
+
+ +
+
); diff --git a/src/components/AIChatWidget.tsx b/src/components/AIChatWidget.tsx new file mode 100644 index 0000000..dd94f7c --- /dev/null +++ b/src/components/AIChatWidget.tsx @@ -0,0 +1,150 @@ +import { useState, useRef, useEffect } from 'react'; +import { Send, Sparkles, Loader2, Bot } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { sendVoiceCommand } from '../services/api'; + +export const AIChatWidget = () => { + const [query, setQuery] = useState(''); + const [messages, setMessages] = useState<{role: 'user' | 'ai', text: string}[]>([]); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const navigate = useNavigate(); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!query.trim()) return; + + const userText = query.trim(); + setQuery(''); + setMessages(prev => [...prev, { role: 'user', text: userText }]); + setIsLoading(true); + + try { + const result = await sendVoiceCommand(userText); + + setMessages(prev => [...prev, { + role: 'ai', + text: result.replyText || "Action completed." + }]); + + handleActionCommand(result.actionCommand); + } catch (error) { + console.error("AI command error:", error); + setMessages(prev => [...prev, { + role: 'ai', + text: "Sorry, there was an error processing your request." + }]); + } finally { + setIsLoading(false); + } + }; + + const handleActionCommand = (actionCommand: string) => { + if (!actionCommand || actionCommand === 'NONE') return; + + if (actionCommand === 'VIEW_LOW_STOCK') { + navigate('/inventory'); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('setFilter', { detail: 'low-stock' })); + }, 500); + } else if (actionCommand === 'VIEW_EXPIRING_STOCK') { + navigate('/inventory'); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('setFilter', { detail: 'expiring' })); + }, 500); + } else if (actionCommand === 'NAVIGATE_DASHBOARD') { + navigate('/'); + } else if (actionCommand === 'NAVIGATE_PHARMACY') { + navigate('/pharmacy'); + } else if (actionCommand === 'NAVIGATE_INVENTORY') { + navigate('/inventory'); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('setFilter', { detail: 'all' })); + }, 500); + } else if (actionCommand === 'NAVIGATE_PRESCRIPTIONS') { + navigate('/prescriptions'); + } else if (actionCommand === 'NAVIGATE_PATIENTS') { + navigate('/patients'); + } else if (actionCommand === 'NAVIGATE_SUPPLIERS') { + navigate('/suppliers'); + } else if (actionCommand === 'NAVIGATE_REGISTERS') { + navigate('/registers'); + } else if (actionCommand === 'NAVIGATE_STOCK_MOVEMENTS') { + navigate('/stock-movements'); + } else if (actionCommand === 'NAVIGATE_HISTORY') { + navigate('/history'); + } + }; + + return ( +
+
+
+ +
+
+

Smart Insights

+

Ask anything about your pharmacy data

+
+
+ +
+ {messages.length === 0 ? ( +
+ +

Try asking "how many patients do we have?"

+

or "show low stock items"

+
+ ) : ( + messages.map((msg, idx) => ( +
+
+ {msg.text} +
+
+ )) + )} + {isLoading && ( +
+
+ +
+
+ )} +
+
+ +
+ setQuery(e.target.value)} + placeholder="Ask AI..." + className="w-full pl-4 pr-12 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 bg-white shadow-sm transition-all text-sm" + disabled={isLoading} + /> + +
+
+ ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..bc0f7cd --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,103 @@ +import { useState, useRef, useEffect } from 'react'; +import { User, Settings, LogOut, ChevronDown } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; +import { useNavigate } from 'react-router-dom'; +import { logoutPharmacist, getMe } from '../services/api'; + +export const Header = () => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [userProfile, setUserProfile] = useState(null); + + console.log("Updated Header Component Loaded v2!"); + + const { logout } = useAuth(); + const navigate = useNavigate(); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + useEffect(() => { + const fetchProfile = async () => { + try { + const data = await getMe(); + if (data && data.profile) { + setUserProfile(data.profile); + } + } catch (err) { + console.error("Failed to load profile in header", err); + } + }; + fetchProfile(); + }, []); + + const handleLogout = async () => { + try { + await logoutPharmacist(); + } catch (e) { + console.error(e); + } finally { + logout(); + navigate('/login'); + } + }; + + return ( +
+
+
+
+ {userProfile?.profile_image_url ? ( + Profile + ) : ( + + )} +
+
+ + {userProfile?.pharmacy_name || userProfile?.username || 'Admin User'} + + + {userProfile?.role || 'Pharmacist'} + +
+
+ + + + {isDropdownOpen && ( +
+ +
+ +
+ )} +
+
+ ); +}; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 91886bc..e751a7d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { NavLink, useNavigate } from 'react-router-dom'; -import { LayoutDashboard, Users, Pill, Settings, HeartPulse, Box, Clock, ClipboardList, Activity, Truck, LogOut } from 'lucide-react'; +import { LayoutDashboard, Users, Pill, Settings, Cross, Box, Clock, ClipboardList, Activity, Truck, LogOut } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { logoutPharmacist } from '../services/api'; @@ -22,7 +22,7 @@ const Sidebar: React.FC = () => { return ( ); }; diff --git a/src/components/VoiceAssistant.tsx b/src/components/VoiceAssistant.tsx index 97b91d9..9ca1349 100644 --- a/src/components/VoiceAssistant.tsx +++ b/src/components/VoiceAssistant.tsx @@ -17,13 +17,13 @@ export const VoiceAssistant: React.FC = () => { const [aiResponse, setAiResponse] = useState(''); const [isPanelOpen, setIsPanelOpen] = useState(false); const navigate = useNavigate(); - + const recognitionRef = useRef(null); useEffect(() => { // Initialize Web Speech API const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; - + if (SpeechRecognition) { recognitionRef.current = new SpeechRecognition(); recognitionRef.current.continuous = false; @@ -46,7 +46,7 @@ export const VoiceAssistant: React.FC = () => { interimTranscript += event.results[i][0].transcript; } } - + setTranscript(finalTranscript || interimTranscript); }; @@ -76,18 +76,18 @@ export const VoiceAssistant: React.FC = () => { // We need a separate effect to trigger processing once listening stops and we have a final transcript useEffect(() => { if (!isListening && transcript) { - processCommand(transcript); + processCommand(transcript); } }, [isListening, transcript]); // Real Intent Parser using Gemini AI backend const processCommand = async (command: string) => { if (!command) return; - + try { setAiResponse("Thinking..."); const result = await sendVoiceCommand(command); - + if (result.replyText) { speak(result.replyText); setAiResponse(result.replyText); @@ -147,7 +147,7 @@ export const VoiceAssistant: React.FC = () => { const toggleListening = () => { if (!recognitionRef.current) return; - + setIsPanelOpen(true); if (isListening) { @@ -173,20 +173,20 @@ export const VoiceAssistant: React.FC = () => {
- +
{transcript && (
{transcript}
)} - + {aiResponse && (
{aiResponse}
)} - + {isListening && !transcript && (
@@ -203,18 +203,17 @@ export const VoiceAssistant: React.FC = () => { {/* Floating Action Button */}
-
+
{recentLogs.length > 0 ? recentLogs.map((log) => { const isReceipt = log.movement_type === 'receipt'; + const isAdjustment = log.movement_type === 'adjustment'; const date = new Date(log.created_at); + + let bgColor = 'bg-pink-100 text-pink-600'; + let icon = ; + let title = 'Medication Dispensed'; + let actionText = 'Dispensed'; + let actionValue = `${Math.abs(log.qty_delta || 0)} units`; + + if (isReceipt) { + bgColor = 'bg-emerald-100 text-emerald-600'; + icon = ; + title = 'Stock Replenished'; + actionText = 'Received'; + } else if (isAdjustment) { + bgColor = 'bg-amber-100 text-amber-600'; + icon = ; + title = 'Stock Adjusted'; + actionText = log.qty_delta < 0 ? 'Decreased by' : 'Increased by'; + } + return ( -
-
- {isReceipt ? : } +
+
+ {icon}

- {isReceipt ? 'Stock Replenished' : 'Medication Dispensed'} + {title}

-

- {isReceipt ? 'Received' : 'Dispensed'} {Math.abs(log.qty_delta)} units (Batch: {log.batch_no}) +

+ {actionText} {actionValue} (Batch: {log.batch_no})

-

+

{date.toLocaleDateString()} • {date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

diff --git a/src/pages/History.tsx b/src/pages/History.tsx index e0cc1d7..b5e00c9 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -3,7 +3,7 @@ import { History as HistoryIcon, Clock, Package, AlertTriangle, Search } from 'l import { createColumnHelper } from '@tanstack/react-table'; import { Table } from '../components/Table'; import Pagination from '../components/Pagination'; -import { getDispensingHistory } from '../services/api'; +import { getDispensingHistory, downloadInvoice } from '../services/api'; interface DispensingHistoryRecord { dispensing_id: string; @@ -93,6 +93,29 @@ const columns = [ ); } }), + columnHelper.display({ + id: 'action', + header: 'ACTION', + size: 150, + cell: info => { + const record = info.row.original; + return ( + + ); + } + }), ]; const History = () => { @@ -158,7 +181,7 @@ const History = () => { {/* Action Bar with Spotlight Search */}
-
+
@@ -168,7 +191,7 @@ const History = () => { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search by patient name, prescription ID or Dispensing ID..." className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
diff --git a/src/pages/Inventory.tsx b/src/pages/Inventory.tsx index 363bf75..c2c8bf3 100644 --- a/src/pages/Inventory.tsx +++ b/src/pages/Inventory.tsx @@ -54,7 +54,7 @@ const columns = [ const days = info.getValue(); let colorClass = "text-emerald-600 bg-emerald-50"; let displayText = `${days} Days`; - + if (days < 0) { colorClass = "text-rose-700 bg-rose-100 border border-rose-200"; displayText = "Expired"; @@ -78,14 +78,19 @@ const columns = [ id: 'actions', header: '', size: 100, - cell: info => ( - - ) + cell: info => { + const isExpired = info.row.original.days_to_expiry !== undefined && info.row.original.days_to_expiry < 0; + if (isExpired) return null; + + return ( + + ); + } }), ]; @@ -134,9 +139,19 @@ const Inventory = () => { if (!selectedBatch || !adjustForm.qty_delta) return; try { setIsAdjusting(true); + let delta = parseInt(adjustForm.qty_delta); + const decreaseReasons = ['damaged', 'expired', 'lost']; + const increaseReasons = ['return']; + + if (decreaseReasons.includes(adjustForm.reason_code)) { + delta = -Math.abs(delta); + } else if (increaseReasons.includes(adjustForm.reason_code)) { + delta = Math.abs(delta); + } + await adjustStock({ batch_no: selectedBatch.batch_no, - qty_delta: parseInt(adjustForm.qty_delta), + qty_delta: delta, reason_code: adjustForm.reason_code, notes: adjustForm.notes }); @@ -155,7 +170,7 @@ const Inventory = () => { try { setIsAddingStock(true); const timestamp = Date.now(); - + let finalProductId = formData.product_id; if (stockMode === 'new') { @@ -186,7 +201,7 @@ const Inventory = () => { product_id: finalProductId, expiry: formData.expiry, qty: formData.qty, - purchase_price: formData.purchase_price + purchase_price: formData.purchase_price || 0 } ] }; @@ -196,12 +211,12 @@ const Inventory = () => { // Refresh the inventory data after successful addition await loadData(); setIsModalOpen(false); // close the modal on success - + // Reset state setStockMode('restock'); setNewMedicine({ brand_name: '', generic_name: '', manufacturer: '', form: 'tablet', strength: '', schedule: 'H', mrp: '', pack_size: '1' }); setFormData({ supplier_id: '', invoice_no: '', product_id: '', batch_no: '', expiry: '', qty: '', purchase_price: '' }); - + alert("Stock added successfully! Inventory count increased."); } catch (err) { console.error("Failed to add stock:", err); @@ -229,12 +244,20 @@ const Inventory = () => { } if (data && data.batches) { - // If filterMode is expiring, we actually want near expiry + // Calculate days_to_expiry manually in case API misses it + const processedBatches = data.batches.map((b: any) => { + if (b.days_to_expiry === undefined || b.days_to_expiry === null) { + b.days_to_expiry = Math.ceil((new Date(b.expiry).getTime() - Date.now()) / (1000 * 3600 * 24)); + } + return b; + }); + if (filterMode === 'expiring') { - const nearExpiry = data.batches.filter((b: any) => b.days_to_expiry && b.days_to_expiry <= 30); + // Show medicines expiring in 90 days, INCLUDING expired (negative or zero) + const nearExpiry = processedBatches.filter((b: any) => b.days_to_expiry <= 90); setInventory(nearExpiry); } else { - setInventory(data.batches); + setInventory(processedBatches); } } @@ -242,14 +265,14 @@ const Inventory = () => { if (drugsData && drugsData.drugs) { setDrugOptions(drugsData.drugs); } - + import('../services/api').then(async (api) => { try { const supps = await api.getSuppliers(); if (supps && supps.suppliers) { setSupplierOptions(supps.suppliers); } - } catch(e) { console.error("Could not fetch suppliers"); } + } catch (e) { console.error("Could not fetch suppliers"); } }); } catch (err: any) { @@ -366,7 +389,7 @@ const Inventory = () => { {/* Action Bar with Spotlight Search */}
-
+
@@ -376,7 +399,7 @@ const Inventory = () => { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search batches by medicine or batch no..." className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
@@ -463,13 +486,13 @@ const Inventory = () => {
- -
- setNewMedicine({...newMedicine, brand_name: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. Crocin Advance" /> + setNewMedicine({ ...newMedicine, brand_name: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. Crocin Advance" />
- setNewMedicine({...newMedicine, generic_name: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. Paracetamol" /> + setNewMedicine({ ...newMedicine, generic_name: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. Paracetamol" />
- setNewMedicine({...newMedicine, manufacturer: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. GSK" /> + setNewMedicine({ ...newMedicine, manufacturer: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. GSK" />
- setNewMedicine({...newMedicine, strength: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 500mg" /> + setNewMedicine({ ...newMedicine, strength: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 500mg" />
- setNewMedicine({...newMedicine, mrp: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 15.50" /> + setNewMedicine({ ...newMedicine, mrp: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 15.50" />
- setNewMedicine({...newMedicine, pack_size: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 10" /> + setNewMedicine({ ...newMedicine, pack_size: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" style={{ padding: '12px 16px' }} placeholder="e.g. 10" />
- setNewMedicine({ ...newMedicine, form: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300" style={{ padding: '12px 16px' }}> @@ -543,7 +566,7 @@ const Inventory = () => {
- setNewMedicine({ ...newMedicine, schedule: e.target.value })} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300" style={{ padding: '12px 16px' }}> @@ -634,23 +657,7 @@ const Inventory = () => {
- {/* Purchase Price */} -
- -
-
- -
- setFormData({ ...formData, purchase_price: e.target.value })} - placeholder="0.00" - className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" - style={{ padding: '12px 16px', paddingLeft: '44px' }} - /> -
-
+
diff --git a/src/pages/Patients.tsx b/src/pages/Patients.tsx index 2d81589..e8acb68 100644 --- a/src/pages/Patients.tsx +++ b/src/pages/Patients.tsx @@ -110,36 +110,32 @@ export default function Patients() { return (
-
+
-

+

Hospital Patient Directory

-

- - Securely Synced via AI Integration -

-
+
- +
{ setSearchQuery(e.target.value); - setCurrentPage(1); // Reset page on search + setCurrentPage(1); }} placeholder="Search by name, ID or contact..." - className="w-full py-2.5 bg-white border border-slate-200 hover:border-slate-300 rounded-xl focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800 text-[14px]" - style={{ paddingLeft: '44px', paddingRight: '16px' }} + className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
-
- {filteredPatients.length} Records +
+ {filteredPatients.length} Patients
diff --git a/src/pages/Pharmacy.tsx b/src/pages/Pharmacy.tsx index bb0d7e1..8dca7fe 100644 --- a/src/pages/Pharmacy.tsx +++ b/src/pages/Pharmacy.tsx @@ -4,7 +4,7 @@ import { createColumnHelper } from '@tanstack/react-table'; import { Table } from '../components/Table'; import Pagination from '../components/Pagination'; import { CustomSelect } from '../components/CustomSelect'; -import { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder } from '../services/api'; +import { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder, downloadInvoice, fetchSyncedPatients } from '../services/api'; interface PharmacyItem { id: string; @@ -103,10 +103,13 @@ const Pharmacy = () => { const [precheckResult, setPrecheckResult] = useState(null); const [isProcessingDispense, setIsProcessingDispense] = useState(false); + const [dispensingId, setDispensingId] = useState(null); + const resetDispenseModal = () => { setIsDispenseModalOpen(false); setDispenseStep(1); setOrderDraftId(null); + setDispensingId(null); setPatientName(''); setSelectedItems([]); setPrecheckResult(null); @@ -140,11 +143,12 @@ const Pharmacy = () => { if (!orderDraftId) return; try { setIsProcessingDispense(true); - await dispenseOrder(orderDraftId, { + const res = await dispenseOrder(orderDraftId, { subject_ref: patientName, prescriber_name: prescriberName, payment_method: 'cash' }); + setDispensingId(res.dispensing_id); setDispenseStep(3); loadData(); // refresh inventory } catch (e: any) { @@ -172,20 +176,28 @@ const Pharmacy = () => { }; const mapAndSetData = (drugs: any[]) => { - const mappedData: PharmacyItem[] = drugs.map((drug: any) => ({ - id: drug.product_id, - medicine: `${drug.brand_name} ${drug.strength || ''}`.trim(), - subtitle: drug.generic_name || '', - badge: drug.schedule || '', - stock: parseInt(drug.total_stock) || 0, - rawPrice: parseFloat(drug.purchase_price) || parseFloat(drug.mrp) || 0, - price: new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(parseFloat(drug.purchase_price) || parseFloat(drug.mrp) || 0), - manufacture: drug.manufacturer || 'Unknown', - type: drug.form || 'Unknown' - })); + const mappedData: PharmacyItem[] = drugs.map((drug: any) => { + const mrp = parseFloat(drug.mrp) || 0; + const packSize = parseInt(drug.pack_size, 10) || 1; + const unitPrice = mrp / packSize; + + return { + id: drug.product_id, + medicine: `${drug.brand_name} ${drug.strength || ''}`.trim(), + subtitle: drug.generic_name || '', + badge: drug.schedule || '', + stock: parseInt(drug.total_stock) || 0, + rawPrice: unitPrice, + price: new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(unitPrice), + manufacture: drug.manufacturer || 'Unknown', + type: drug.form || 'Unknown' + }; + }); setInventory(mappedData); }; + const [hospitalPatients, setHospitalPatients] = useState([]); + const loadData = async () => { try { setIsLoading(true); @@ -194,10 +206,17 @@ const Pharmacy = () => { await authenticate(); } - const data = await getDrugs(); + const [data, patients] = await Promise.all([ + getDrugs(), + fetchSyncedPatients().catch(() => []) // fail gracefully if hospital api is down + ]); + if (data && data.drugs) { mapAndSetData(data.drugs); } + if (patients) { + setHospitalPatients(patients); + } } catch (error: any) { if (error.response && error.response.status === 401) { try { @@ -206,12 +225,14 @@ const Pharmacy = () => { if (retryData && retryData.drugs) { mapAndSetData(retryData.drugs); } + const retryPatients = await fetchSyncedPatients().catch(() => []); + setHospitalPatients(retryPatients); } catch (retryError) { - console.error("Failed to fetch drugs after retry:", retryError); + console.error("Failed to fetch data after retry:", retryError); setError("Authentication failed. Please try logging in again."); } } else { - console.error("Failed to fetch drugs:", error); + console.error("Failed to fetch data:", error); setError("Failed to connect to the server. Please try again later."); } } finally { @@ -221,6 +242,12 @@ const Pharmacy = () => { useEffect(() => { loadData(); + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('action') === 'dispense') { + setIsDispenseModalOpen(true); + // Clean up URL after opening + window.history.replaceState({}, '', '/pharmacy'); + } }, []); // Filter inventory based on search query @@ -299,7 +326,7 @@ const Pharmacy = () => { {/* Action Bar */}
-
+
@@ -309,19 +336,11 @@ const Pharmacy = () => { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search medicines, OTC, or prescriptions..." className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
-
- -
+
@@ -471,15 +490,21 @@ const Pharmacy = () => {
- + setPatientName(e.target.value)} + list="patients-list" className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" style={{ padding: '12px 16px' }} placeholder="e.g. John Doe" /> + + {hospitalPatients.map((p, idx) => ( + + ))} +
@@ -597,13 +622,29 @@ const Pharmacy = () => {

Dispense Successful!

Stock has been deducted and logged.

- +
+ + +
)}
diff --git a/src/pages/Prescriptions.tsx b/src/pages/Prescriptions.tsx index 8a230ab..8d9054d 100644 --- a/src/pages/Prescriptions.tsx +++ b/src/pages/Prescriptions.tsx @@ -340,17 +340,17 @@ const Prescriptions = () => { ); return ( -
-
-

AI Prescription Processing

-

Upload a scanned prescription and let the AI extract the medical details.

+
+
+

AI Prescription Processing

+

Upload a scanned prescription and let the AI extract the medical details.

{/* Upload Column */} -
-

Upload Scan

+
+

Upload Scan

@@ -374,18 +374,18 @@ const Prescriptions = () => {
{file && ( -
-
- -
-

{file.name}

+
+
+ +
+

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

{!isUploading && !isPolling && !prescriptionData && (
diff --git a/src/pages/Registers.tsx b/src/pages/Registers.tsx index 4d1b4d3..a03d2ee 100644 --- a/src/pages/Registers.tsx +++ b/src/pages/Registers.tsx @@ -132,7 +132,7 @@ const Registers = () => {
-
+
@@ -142,7 +142,7 @@ const Registers = () => { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search by patient, drug, or doctor..." className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
diff --git a/src/pages/StockMovements.tsx b/src/pages/StockMovements.tsx index 0e4e9a2..64f0c33 100644 --- a/src/pages/StockMovements.tsx +++ b/src/pages/StockMovements.tsx @@ -3,7 +3,7 @@ import { getAuditLogs, authenticate } from '../services/api'; import { Table } from '../components/Table'; import Pagination from '../components/Pagination'; import { createColumnHelper } from '@tanstack/react-table'; -import { Activity, ArrowDownRight, ArrowUpRight, Clock, Package, Search } from 'lucide-react'; +import { Activity, ArrowDownRight, ArrowUpRight, Clock, Package, Search, AlertCircle } from 'lucide-react'; const columnHelper = createColumnHelper(); @@ -27,16 +27,28 @@ const columns = [ header: 'Movement Type', size: 200, cell: info => { - const type = info.getValue(); + const type = info.getValue() || ''; const isReceipt = type === 'receipt'; + const isAdjustment = type === 'adjustment'; + + let bgClass = 'bg-pink-100 text-pink-700 border-pink-200'; + let icon = ; + let label = 'Dispensed'; + + if (isReceipt) { + bgClass = 'bg-emerald-100 text-emerald-700 border-emerald-200'; + icon = ; + label = 'Stock Received'; + } else if (isAdjustment) { + bgClass = 'bg-amber-100 text-amber-700 border-amber-200'; + icon = ; + label = 'Adjustment'; + } + return ( -
- {isReceipt ? : } - {isReceipt ? 'Stock Received' : 'Dispensed'} +
+ {icon} + {label}
); } @@ -63,6 +75,31 @@ const columns = [ ); } + }), + columnHelper.accessor('notes', { + header: 'Details / Notes', + size: 250, + cell: info => { + const row = info.row.original; + const notes = row.notes; + const reasonCode = row.reason_code; + + if (!notes && !reasonCode) return -; + + const safeReasonCode = reasonCode ? String(reasonCode).replace(/_/g, ' ') : ''; + const safeNotes = notes ? String(notes) : ''; + + return ( +
+ {safeReasonCode && ( + + {safeReasonCode} + + )} + {safeNotes && {safeNotes}} +
+ ); + } }) ]; @@ -81,8 +118,11 @@ export default function StockMovements() { } const logsData = await getAuditLogs(50); // fetch more logs for the table + console.log('logsData', logsData); if (logsData && logsData.stock_movements) { setLogs(logsData.stock_movements); + } else if (logsData && logsData.logs) { + setLogs(logsData.logs); } } catch (err) { console.error('Failed to load stock movements:', err); @@ -129,17 +169,17 @@ export default function StockMovements() {
-
-
- +
+
+
setSearchQuery(e.target.value)} placeholder="Search by batch no or type..." - className="w-full py-2.5 bg-white border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px' }} + className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
diff --git a/src/pages/Suppliers.tsx b/src/pages/Suppliers.tsx index 1013b19..70d0af1 100644 --- a/src/pages/Suppliers.tsx +++ b/src/pages/Suppliers.tsx @@ -177,7 +177,7 @@ const Suppliers = () => { {/* Action Bar */}
-
+
@@ -187,7 +187,7 @@ const Suppliers = () => { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search suppliers by name, contact, DL or GSTIN..." className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800" - style={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} />
diff --git a/src/pages/settings/ProfileSettings.tsx b/src/pages/settings/ProfileSettings.tsx index 54743c2..d45aa4b 100644 --- a/src/pages/settings/ProfileSettings.tsx +++ b/src/pages/settings/ProfileSettings.tsx @@ -1,10 +1,12 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { getMe, updateMe } from '../../services/api'; -import { Save, User, CheckCircle, ArrowLeft, Building2, FileText, Hash, MapPin, Shield } from 'lucide-react'; +import { Save, User, CheckCircle, ArrowLeft, Building2, FileText, Hash, MapPin, Shield, Camera, UploadCloud } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; const ProfileSettings = () => { const navigate = useNavigate(); + const fileInputRef = useRef(null); + const [profile, setProfile] = useState({ username: '', role: '', @@ -12,11 +14,16 @@ const ProfileSettings = () => { dl_number: '', gstin: '', pharmacist_reg_number: '', - address: '' + address: '', + profile_image_url: '' }); + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const [isUploading, setIsUploading] = useState(false); const [message, setMessage] = useState(''); useEffect(() => { @@ -31,7 +38,8 @@ const ProfileSettings = () => { dl_number: data.profile.dl_number || '', gstin: data.profile.gstin || '', pharmacist_reg_number: data.profile.pharmacist_reg_number || '', - address: data.profile.address || '' + address: data.profile.address || '', + profile_image_url: data.profile.profile_image_url || '' }); } } catch (err) { @@ -48,18 +56,45 @@ const ProfileSettings = () => { setProfile(prev => ({ ...prev, [name]: value })); }; + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setSelectedFile(file); + // Create local preview URL + const url = URL.createObjectURL(file); + setPreviewUrl(url); + } + }; + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); try { setIsSaving(true); setMessage(''); - const data = await updateMe({ - pharmacy_name: profile.pharmacy_name, - dl_number: profile.dl_number, - gstin: profile.gstin, - pharmacist_reg_number: profile.pharmacist_reg_number, - address: profile.address - }); + + const formData = new FormData(); + formData.append('pharmacy_name', profile.pharmacy_name); + formData.append('dl_number', profile.dl_number); + formData.append('gstin', profile.gstin); + formData.append('pharmacist_reg_number', profile.pharmacist_reg_number); + formData.append('address', profile.address); + if (profile.profile_image_url) { + formData.append('profile_image_url', profile.profile_image_url); + } + + if (selectedFile) { + formData.append('profileImage', selectedFile); + } + + const data = await updateMe(formData); + + if (data && data.profile) { + setProfile(prev => ({ + ...prev, + profile_image_url: data.profile.profile_image_url || prev.profile_image_url + })); + } + setMessage('Profile updated successfully!'); setTimeout(() => setMessage(''), 3000); } catch (err) { @@ -113,6 +148,47 @@ const ProfileSettings = () => {
+ + {/* Profile Image Upload Section */} +
+
+
+ {(previewUrl || profile.profile_image_url) ? ( + Profile + ) : ( + + )} +
+ {isUploading && ( +
+
+
+ )} +
+
+

Pharmacy Logo

+

+ Upload your pharmacy logo or profile picture. This will appear on invoices and reports. +

+ + +
+
+
@@ -229,10 +305,14 @@ const ProfileSettings = () => {
diff --git a/src/services/api.ts b/src/services/api.ts index b880a44..7958644 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,20 +1,25 @@ import axios from 'axios'; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/v1'; + // Create a base Axios instance for API calls export const api = axios.create({ - baseURL: 'http://localhost:8080/v1', // Backend port is 8080 + baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', + 'x-api-key': import.meta.env.VITE_API_KEY || '', }, }); // Authenticate and retrieve token export const authenticate = async () => { try { - const response = await axios.post('http://localhost:8080/v1/auth/token', { + const response = await axios.post(`${API_BASE_URL}/auth/token`, { grant_type: 'client_credentials', client_id: 'jayam_web_ai', client_secret: 'my_secret_key' + }, { + headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' } }); // Assuming the response contains an access_token field @@ -33,7 +38,9 @@ export const authenticate = async () => { // Login with username and password export const loginPharmacist = async (credentials: any) => { try { - const response = await axios.post('http://localhost:8080/v1/auth/login', credentials); + const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials, { + headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' } + }); const data = response.data; if (data.token) { @@ -54,7 +61,9 @@ export const loginPharmacist = async (credentials: any) => { export const logoutPharmacist = async () => { try { // Call the logout endpoint - const response = await axios.post('http://localhost:8080/v1/auth/logout'); + const response = await axios.post(`${API_BASE_URL}/auth/logout`, {}, { + headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' } + }); return response.data; } catch (error) { console.error('Logout API failed:', error); @@ -67,8 +76,11 @@ export const getMe = async () => { return response.data; }; -export const updateMe = async (data: any) => { - const response = await api.put('/auth/me', data); +export const updateMe = async (data: any | FormData) => { + const isFormData = data instanceof FormData; + const response = await api.put('/auth/me', data, { + headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined + }); return response.data; }; @@ -101,6 +113,24 @@ api.interceptors.request.use((config) => { return Promise.reject(error); }); +// Response interceptor to handle global errors like 401 Unauthorized +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + // Do not redirect if we are already on the login page or trying to login + if (window.location.pathname !== '/login' && error.config && !error.config.url.includes('/auth/login')) { + // Token is invalid or expired + localStorage.removeItem('auth_token'); + localStorage.removeItem('user'); + localStorage.removeItem('actor_id'); + window.location.href = '/login'; + } + } + return Promise.reject(error); + } +); + export const getDrugs = async () => { const response = await api.get('/drugs'); return response.data; @@ -254,4 +284,19 @@ export const fetchSyncedPatients = async () => { return response.data.data; }; +export const downloadInvoice = async (id: string) => { + const response = await api.get(`/orders/${id}/invoice/pdf`, { + responseType: 'blob' + }); + + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `invoice-${id}.pdf`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +}; + export default api; diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..3d15f68 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + tailwindcss(), + ], })