add AI voice assistant, chat widget, inventory and pharmacy pages with API services
This commit is contained in:
Generated
+11
-11
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
+58
-8
@@ -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<PharmacyItem[]>([]);
|
||||
@@ -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 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
navigate('/pharmacy', { replace: true });
|
||||
}}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-600 transition-colors"
|
||||
title="Clear search"
|
||||
type="button"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user