From 6c43b92671151f95d6a88383fee3ed751cef44b6 Mon Sep 17 00:00:00 2001 From: mohamedshahil-csh Date: Fri, 18 Sep 2026 10:39:03 +0530 Subject: [PATCH] add AI voice assistant, chat widget, inventory and pharmacy pages with API services --- package-lock.json | 22 +++++------ src/components/AIChatWidget.tsx | 28 +++++++++++-- src/components/VoiceAssistant.tsx | 23 +++++++++-- src/pages/Inventory.tsx | 7 ++++ src/pages/Pharmacy.tsx | 66 +++++++++++++++++++++++++++---- src/services/api.ts | 2 +- tsconfig.app.json | 4 +- 7 files changed, 123 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c8c5cd..12cd965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,16 +65,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1419,6 +1409,7 @@ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -1429,6 +1420,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1575,6 +1567,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", @@ -2548,6 +2541,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2574,6 +2568,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", @@ -2604,6 +2599,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2613,6 +2609,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2632,6 +2629,7 @@ "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" @@ -2722,7 +2720,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -2929,6 +2928,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", diff --git a/src/components/AIChatWidget.tsx b/src/components/AIChatWidget.tsx index dd94f7c..d57d43f 100644 --- a/src/components/AIChatWidget.tsx +++ b/src/components/AIChatWidget.tsx @@ -35,7 +35,7 @@ export const AIChatWidget = () => { text: result.replyText || "Action completed." }]); - handleActionCommand(result.actionCommand); + handleActionCommand(result.actionCommand, result.searchTerm, userText); } catch (error) { console.error("AI command error:", error); setMessages(prev => [...prev, { @@ -47,7 +47,29 @@ export const AIChatWidget = () => { } }; - const handleActionCommand = (actionCommand: string) => { + const handleActionCommand = (actionCommand: string, apiSearchTerm?: string, originalText?: string) => { + // Extract search term from backend response or user input + let searchTerm = apiSearchTerm; + if (!searchTerm && originalText) { + const match = originalText.match(/(?:how much (?:quantity|stock) (?:in|of)|quantity of|stock of|search for|search|find|do we have|is there any)\s+([a-zA-Z0-9\s\-_]+)/i); + if (match && match[1]) { + searchTerm = match[1].trim(); + } + } + + if (actionCommand === 'NAVIGATE_PHARMACY' || actionCommand === 'SEARCH_MEDICINE' || searchTerm) { + if (searchTerm) { + navigate(`/pharmacy?search=${encodeURIComponent(searchTerm)}`); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('setPharmacySearch', { detail: searchTerm })); + window.dispatchEvent(new CustomEvent('setSearchQuery', { detail: searchTerm })); + }, 200); + } else { + navigate('/pharmacy'); + } + return; + } + if (!actionCommand || actionCommand === 'NONE') return; if (actionCommand === 'VIEW_LOW_STOCK') { @@ -62,8 +84,6 @@ export const AIChatWidget = () => { }, 500); } else if (actionCommand === 'NAVIGATE_DASHBOARD') { navigate('/'); - } else if (actionCommand === 'NAVIGATE_PHARMACY') { - navigate('/pharmacy'); } else if (actionCommand === 'NAVIGATE_INVENTORY') { navigate('/inventory'); setTimeout(() => { diff --git a/src/components/VoiceAssistant.tsx b/src/components/VoiceAssistant.tsx index 9ca1349..b97d873 100644 --- a/src/components/VoiceAssistant.tsx +++ b/src/components/VoiceAssistant.tsx @@ -95,7 +95,26 @@ export const VoiceAssistant: React.FC = () => { setAiResponse("I didn't quite understand that."); } - if (result.actionCommand === 'VIEW_LOW_STOCK') { + // Extract search term from backend response or transcript + let searchTerm = result.searchTerm; + if (!searchTerm) { + const match = command.match(/(?:how much (?:quantity|stock) (?:in|of)|quantity of|stock of|search for|search|find|do we have|is there any)\s+([a-zA-Z0-9\s\-_]+)/i); + if (match && match[1]) { + searchTerm = match[1].trim(); + } + } + + if (result.actionCommand === 'NAVIGATE_PHARMACY' || result.actionCommand === 'SEARCH_MEDICINE' || searchTerm) { + if (searchTerm) { + navigate(`/pharmacy?search=${encodeURIComponent(searchTerm)}`); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('setPharmacySearch', { detail: searchTerm })); + window.dispatchEvent(new CustomEvent('setSearchQuery', { detail: searchTerm })); + }, 200); + } else { + navigate('/pharmacy'); + } + } else if (result.actionCommand === 'VIEW_LOW_STOCK') { navigate('/inventory'); setTimeout(() => { const event = new CustomEvent('setFilter', { detail: 'low-stock' }); @@ -109,8 +128,6 @@ export const VoiceAssistant: React.FC = () => { }, 500); } else if (result.actionCommand === 'NAVIGATE_DASHBOARD') { navigate('/'); - } else if (result.actionCommand === 'NAVIGATE_PHARMACY') { - navigate('/pharmacy'); } else if (result.actionCommand === 'NAVIGATE_INVENTORY') { navigate('/inventory'); setTimeout(() => { diff --git a/src/pages/Inventory.tsx b/src/pages/Inventory.tsx index cb5ff59..0f5d421 100644 --- a/src/pages/Inventory.tsx +++ b/src/pages/Inventory.tsx @@ -314,11 +314,18 @@ const Inventory = () => { setIsAdjustModalOpen(true); } }; + const handleSetSearch = (e: any) => { + if (typeof e.detail === 'string') { + setSearchQuery(e.detail); + } + }; window.addEventListener('setFilter', handleSetFilter); window.addEventListener('openAdjustModal', handleOpenAdjust); + window.addEventListener('setSearchQuery', handleSetSearch); return () => { window.removeEventListener('setFilter', handleSetFilter); window.removeEventListener('openAdjustModal', handleOpenAdjust); + window.removeEventListener('setSearchQuery', handleSetSearch); }; }, []); diff --git a/src/pages/Pharmacy.tsx b/src/pages/Pharmacy.tsx index 12a54d1..913a3be 100644 --- a/src/pages/Pharmacy.tsx +++ b/src/pages/Pharmacy.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import { Search, AlertCircle, Package, Layers, Plus, X, ChevronDown } from 'lucide-react'; import { createColumnHelper } from '@tanstack/react-table'; import { Table } from '../components/Table'; @@ -73,6 +74,8 @@ const columns = [ ]; const Pharmacy = () => { + const location = useLocation(); + const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(25); const [inventory, setInventory] = useState([]); @@ -246,18 +249,52 @@ const Pharmacy = () => { 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 + // Listen for external search triggers (e.g., Voice Assistant or Chat Widget) + useEffect(() => { + const handleSearchEvent = (e: any) => { + if (typeof e.detail === 'string') { + setSearchQuery(e.detail); + } + }; + window.addEventListener('setPharmacySearch', handleSearchEvent); + window.addEventListener('setSearchQuery', handleSearchEvent); + + return () => { + window.removeEventListener('setPharmacySearch', handleSearchEvent); + window.removeEventListener('setSearchQuery', handleSearchEvent); + }; + }, []); + + // Automatically sync search query when navigating with ?search=query + useEffect(() => { + const params = new URLSearchParams(location.search); + const searchParam = params.get('search'); + if (searchParam !== null) { + setSearchQuery(searchParam); + } + }, [location.search]); + + // Filter inventory based on search query with smart matching const filteredInventory = useMemo(() => { - return inventory.filter(item => - item.medicine.toLowerCase().includes(searchQuery.toLowerCase()) || - item.subtitle.toLowerCase().includes(searchQuery.toLowerCase()) || - item.manufacture.toLowerCase().includes(searchQuery.toLowerCase()) - ); + if (!searchQuery.trim()) return inventory; + const cleanSearch = searchQuery.trim().toLowerCase(); + const compactSearch = cleanSearch.replace(/[\s\-_]+/g, ''); + + return inventory.filter(item => { + const med = item.medicine.toLowerCase(); + const sub = item.subtitle.toLowerCase(); + const mfr = item.manufacture.toLowerCase(); + const compactMed = med.replace(/[\s\-_]+/g, ''); + + return med.includes(cleanSearch) || + sub.includes(cleanSearch) || + mfr.includes(cleanSearch) || + compactMed.includes(compactSearch); + }); }, [inventory, searchQuery]); // Reset pagination when search changes @@ -337,8 +374,21 @@ 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={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }} + style={{ height: '46px', paddingLeft: '44px', paddingRight: searchQuery ? '40px' : '16px', fontSize: '15px' }} /> + {searchQuery && ( + + )} diff --git a/src/services/api.ts b/src/services/api.ts index 7958644..8f24795 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/v1'; +const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080').trim().replace(/\/+$/, ''); // Create a base Axios instance for API calls export const api = axios.create({ diff --git a/tsconfig.app.json b/tsconfig.app.json index 6830b6f..49c6746 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -17,8 +17,8 @@ "jsx": "react-jsx", /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true },