feat: update frontend components and page layouts
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
VITE_API_BASE_URL=http://localhost:8080/v1
|
VITE_API_BASE_URL=http://localhost:8080/v1
|
||||||
|
|
||||||
VITE_API_KEY=915e46784e798f21759e52bd2a9d4cc2
|
VITE_API_KEY=DUMUqJrNeyuaPDSZSsMf91qjgwc5K3N7
|
||||||
|
|||||||
Generated
+11
-11
@@ -65,6 +65,16 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
@@ -1409,7 +1419,6 @@
|
|||||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
@@ -1420,7 +1429,6 @@
|
|||||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1567,7 +1575,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.44",
|
"baseline-browser-mapping": "^2.10.44",
|
||||||
"caniuse-lite": "^1.0.30001806",
|
"caniuse-lite": "^1.0.30001806",
|
||||||
@@ -2541,7 +2548,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2568,7 +2574,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.16",
|
"nanoid": "^3.3.16",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -2599,7 +2604,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2609,7 +2613,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -2629,7 +2632,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
|
||||||
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
|
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@@ -2720,8 +2722,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -2928,7 +2929,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
|
|||||||
@@ -79,18 +79,18 @@ export const Header = () => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<div className="absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-slate-200 p-2 animate-in fade-in slide-in-from-top-2">
|
<div className="absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-slate-200 animate-in fade-in slide-in-from-top-2" style={{ padding: '8px' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setIsDropdownOpen(false); navigate('/settings'); }}
|
onClick={() => { setIsDropdownOpen(false); navigate('/settings'); }}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 rounded-lg transition-colors"
|
className="w-full flex items-center gap-3 text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 rounded-lg transition-colors" style={{ padding: '10px 12px' }}
|
||||||
>
|
>
|
||||||
<Settings size={18} className="text-slate-400" />
|
<Settings size={18} className="text-slate-400" />
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-slate-100 my-3 mx-2"></div>
|
<div className="h-px bg-slate-100" style={{ margin: '4px 0' }}></div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium text-rose-600 hover:bg-rose-50 hover:text-rose-700 rounded-lg transition-colors"
|
className="w-full flex items-center gap-3 text-sm font-medium text-rose-600 hover:bg-rose-50 hover:text-rose-700 rounded-lg transition-colors" style={{ padding: '10px 12px' }}
|
||||||
>
|
>
|
||||||
<LogOut size={18} />
|
<LogOut size={18} />
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { Search, ChevronDown, AlertTriangle, Check, X, Clock, Pill } from 'lucide-react';
|
||||||
|
import { getExpiringInventory, getDrugs } from '../services/api';
|
||||||
|
|
||||||
|
export interface StockItem {
|
||||||
|
id?: string;
|
||||||
|
product_id?: string;
|
||||||
|
batch_no?: string;
|
||||||
|
brand_name?: string;
|
||||||
|
generic_name?: string;
|
||||||
|
qty_on_hand?: number;
|
||||||
|
qty?: number;
|
||||||
|
expiry?: string;
|
||||||
|
days_to_expiry?: number;
|
||||||
|
price?: string | number;
|
||||||
|
mrp?: string | number;
|
||||||
|
purchase_price?: string | number;
|
||||||
|
manufacturer?: string;
|
||||||
|
form?: string;
|
||||||
|
schedule?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockDropdownProps {
|
||||||
|
/** Selected value (product_id or batch_no depending on mode) */
|
||||||
|
value?: string;
|
||||||
|
/** Callback fired on selection change */
|
||||||
|
onChange?: (value: string, selectedItem: StockItem | null) => void;
|
||||||
|
/** Mode to display items: 'product' for drug catalog, 'batch' for inventory batch list */
|
||||||
|
mode?: 'product' | 'batch';
|
||||||
|
/** External list of options. If provided, disables internal auto-fetching unless autoFetch is true */
|
||||||
|
options?: StockItem[];
|
||||||
|
/** Placeholder text */
|
||||||
|
placeholder?: string;
|
||||||
|
/** Label above the dropdown */
|
||||||
|
label?: string;
|
||||||
|
/** Error message string */
|
||||||
|
error?: string;
|
||||||
|
/** Disable the dropdown */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** Optional custom class for outer wrapper */
|
||||||
|
className?: string;
|
||||||
|
/** Show stock level badges (In Stock, Low Stock, Out of Stock) */
|
||||||
|
showStockBadge?: boolean;
|
||||||
|
/** Show price / MRP in options */
|
||||||
|
showPrice?: boolean;
|
||||||
|
/** Automatically fetch data from backend if options not supplied */
|
||||||
|
autoFetch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StockDropdown({
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
mode = 'product',
|
||||||
|
options: externalOptions,
|
||||||
|
placeholder = 'Select stock item...',
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
showStockBadge = true,
|
||||||
|
showPrice = true,
|
||||||
|
autoFetch = true
|
||||||
|
}: StockDropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [internalItems, setInternalItems] = useState<StockItem[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Fetch data if external options are not provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (externalOptions && externalOptions.length > 0) {
|
||||||
|
setInternalItems(externalOptions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoFetch) return;
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (mode === 'batch') {
|
||||||
|
const res = await getExpiringInventory();
|
||||||
|
if (isMounted && res && res.batches) {
|
||||||
|
setInternalItems(res.batches);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await getDrugs();
|
||||||
|
if (isMounted && res && res.drugs) {
|
||||||
|
setInternalItems(res.drugs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('StockDropdown failed to fetch stock options:', err);
|
||||||
|
} finally {
|
||||||
|
if (isMounted) setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [externalOptions, mode, autoFetch]);
|
||||||
|
|
||||||
|
// Use external or internal options
|
||||||
|
const itemList = useMemo(() => {
|
||||||
|
return externalOptions || internalItems;
|
||||||
|
}, [externalOptions, internalItems]);
|
||||||
|
|
||||||
|
// Helper to get uniquely identifying key for each item
|
||||||
|
const getItemKey = (item: StockItem): string => {
|
||||||
|
if (mode === 'batch') {
|
||||||
|
return item.batch_no || item.id || item.product_id || '';
|
||||||
|
}
|
||||||
|
return item.product_id || item.id || item.brand_name || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Currently selected item
|
||||||
|
const selectedItem = useMemo(() => {
|
||||||
|
if (!value) return null;
|
||||||
|
return itemList.find(item => getItemKey(item) === value) || null;
|
||||||
|
}, [itemList, value, mode]);
|
||||||
|
|
||||||
|
// Filter items based on search input
|
||||||
|
const filteredItems = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return itemList;
|
||||||
|
const q = searchQuery.toLowerCase().trim();
|
||||||
|
return itemList.filter(item => {
|
||||||
|
const brand = (item.brand_name || '').toLowerCase();
|
||||||
|
const generic = (item.generic_name || '').toLowerCase();
|
||||||
|
const batch = (item.batch_no || '').toLowerCase();
|
||||||
|
const productId = (item.product_id || item.id || '').toLowerCase();
|
||||||
|
const manufacturer = (item.manufacturer || '').toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
brand.includes(q) ||
|
||||||
|
generic.includes(q) ||
|
||||||
|
batch.includes(q) ||
|
||||||
|
productId.includes(q) ||
|
||||||
|
manufacturer.includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [itemList, searchQuery]);
|
||||||
|
|
||||||
|
// Handle outside click to close dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Focus search input on open
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && searchInputRef.current) {
|
||||||
|
setTimeout(() => searchInputRef.current?.focus(), 50);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSelect = (item: StockItem) => {
|
||||||
|
const key = getItemKey(item);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(key, item);
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onChange) {
|
||||||
|
onChange('', null);
|
||||||
|
}
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStockBadge = (item: StockItem) => {
|
||||||
|
const qty = item.qty_on_hand !== undefined ? item.qty_on_hand : item.qty;
|
||||||
|
if (qty === undefined || qty === null) return null;
|
||||||
|
|
||||||
|
if (qty <= 0) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold bg-rose-100 text-rose-700 border border-rose-200">
|
||||||
|
Out of Stock
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (qty < 50) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold bg-amber-100 text-amber-800 border border-amber-200">
|
||||||
|
Low ({qty})
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold bg-emerald-100 text-emerald-800 border border-emerald-200">
|
||||||
|
In Stock ({qty})
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col w-full text-left ${className}`} ref={containerRef}>
|
||||||
|
{label && (
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500 mb-2">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Select Trigger Button */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => !disabled && setIsOpen(prev => !prev)}
|
||||||
|
className={`
|
||||||
|
w-full flex items-center justify-between gap-3 px-4 py-3 rounded-xl border text-left transition-all duration-200 shadow-sm
|
||||||
|
${disabled ? 'bg-slate-100 text-slate-400 cursor-not-allowed border-slate-200' : 'bg-slate-50 hover:bg-slate-100/80 border-slate-200 focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500'}
|
||||||
|
${error ? 'border-rose-400 ring-2 ring-rose-500/10' : ''}
|
||||||
|
`}
|
||||||
|
style={{ minHeight: '48px' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<div className="p-2 bg-blue-100/70 text-blue-600 rounded-lg flex-shrink-0">
|
||||||
|
<Pill size={18} strokeWidth={2.2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedItem ? (
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-slate-900 text-[15px] truncate">
|
||||||
|
{selectedItem.brand_name || selectedItem.generic_name || 'Selected Item'}
|
||||||
|
</span>
|
||||||
|
{mode === 'batch' && selectedItem.batch_no && (
|
||||||
|
<span className="text-[11px] bg-slate-200/80 text-slate-700 font-mono px-2 py-0.5 rounded font-bold">
|
||||||
|
{selectedItem.batch_no}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500 mt-0.5">
|
||||||
|
{selectedItem.generic_name && (
|
||||||
|
<span className="truncate text-slate-500">{selectedItem.generic_name}</span>
|
||||||
|
)}
|
||||||
|
{showStockBadge && getStockBadge(selectedItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-400 text-sm font-medium truncate">
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 flex-shrink-0 text-slate-400">
|
||||||
|
{selectedItem && !disabled && (
|
||||||
|
<span
|
||||||
|
onClick={handleClear}
|
||||||
|
className="p-1 hover:bg-slate-200 rounded-full transition-colors text-slate-400 hover:text-slate-600 cursor-pointer"
|
||||||
|
title="Clear selection"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
size={18}
|
||||||
|
strokeWidth={2.2}
|
||||||
|
className={`transition-transform duration-200 ${isOpen ? 'rotate-180 text-blue-600' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Popover Card */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute z-50 left-0 right-0 mt-2 bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
|
|
||||||
|
{/* Search Header Bar */}
|
||||||
|
<div className="p-3.5 border-b border-slate-200/80 bg-slate-50/80 backdrop-blur-md">
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: '14px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
color: '#94a3b8'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search size={18} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search brand, generic name, or batch..."
|
||||||
|
className="w-full bg-white border border-slate-200 hover:border-slate-300 rounded-xl text-sm font-medium text-slate-800 focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all"
|
||||||
|
style={{
|
||||||
|
paddingLeft: '44px',
|
||||||
|
paddingRight: '40px',
|
||||||
|
paddingTop: '10px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{searchQuery && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="p-1 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Item Options List */}
|
||||||
|
<div className="max-h-72 overflow-y-auto p-2 space-y-1">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-slate-400 text-sm flex items-center justify-center gap-3">
|
||||||
|
<div className="w-5 h-5 border-2 border-slate-200 border-t-blue-600 rounded-full animate-spin"></div>
|
||||||
|
<span className="font-medium">Loading stock items...</span>
|
||||||
|
</div>
|
||||||
|
) : filteredItems.length === 0 ? (
|
||||||
|
<div className="p-8 text-center text-slate-400 text-sm font-medium">
|
||||||
|
No stock items match "{searchQuery}"
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredItems.map(item => {
|
||||||
|
const key = getItemKey(item);
|
||||||
|
const isSelected = key === value;
|
||||||
|
const mrp = item.mrp || item.price || item.purchase_price;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleSelect(item)}
|
||||||
|
className={`
|
||||||
|
px-4 py-3 cursor-pointer rounded-xl transition-all duration-150 flex items-center justify-between gap-4 border
|
||||||
|
${isSelected
|
||||||
|
? 'bg-blue-50/90 border-blue-200/90 text-blue-950 shadow-sm'
|
||||||
|
: 'border-transparent hover:bg-slate-100/70 hover:border-slate-200/60'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col min-w-0 flex-1 gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-slate-900 text-[14px] leading-tight truncate">
|
||||||
|
{item.brand_name || item.generic_name || 'Unnamed Item'}
|
||||||
|
</span>
|
||||||
|
{mode === 'batch' && item.batch_no && (
|
||||||
|
<span className="text-[11px] bg-slate-100 text-slate-700 font-mono px-2 py-0.5 rounded font-bold border border-slate-200">
|
||||||
|
{item.batch_no}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.generic_name && (
|
||||||
|
<span className="text-xs text-slate-500 font-medium truncate">
|
||||||
|
{item.generic_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mt-1 text-xs text-slate-500">
|
||||||
|
{showStockBadge && getStockBadge(item)}
|
||||||
|
|
||||||
|
{item.expiry && (
|
||||||
|
<span className="flex items-center gap-1 text-[11px] font-medium text-slate-500 bg-slate-100 px-2 py-0.5 rounded-md border border-slate-200/60">
|
||||||
|
<Clock size={12} className="text-slate-400" /> Exp: {new Date(item.expiry).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.form && (
|
||||||
|
<span className="text-[11px] uppercase tracking-wider font-bold text-slate-400 bg-slate-100/80 px-1.5 py-0.5 rounded">
|
||||||
|
{item.form}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 flex-shrink-0">
|
||||||
|
{showPrice && mrp !== undefined && mrp !== null && (
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-400 block">MRP</span>
|
||||||
|
<span className="font-extrabold text-slate-900 text-sm">
|
||||||
|
₹{Number(mrp).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<div className="p-1 bg-blue-600 text-white rounded-full shadow-sm">
|
||||||
|
<Check size={14} strokeWidth={3} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Status Bar */}
|
||||||
|
<div className="px-4 py-2.5 bg-slate-50 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
|
||||||
|
<span className="font-semibold text-slate-600">{filteredItems.length} available items</span>
|
||||||
|
<span className="text-[11px] text-slate-400 font-bold uppercase tracking-wider">Mode: {mode}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="mt-1.5 text-xs font-semibold text-rose-500 flex items-center gap-1">
|
||||||
|
<AlertTriangle size={12} /> {error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StockDropdown;
|
||||||
@@ -8,7 +8,7 @@ import { MoreHorizontal } from 'lucide-react';
|
|||||||
export interface TableProps<T> {
|
export interface TableProps<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
columns: any[];
|
columns: any[];
|
||||||
getRowId?: (row: T) => string;
|
getRowId?: (row: T, index?: number) => string;
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,13 @@ export const Table = <T,>({ data, columns, getRowId, emptyMessage = 'No data ava
|
|||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getRowId,
|
getRowId: (row: any, index: number) => {
|
||||||
|
let customId = getRowId ? getRowId(row) : null;
|
||||||
|
if (customId !== undefined && customId !== null && customId !== '') {
|
||||||
|
return `${customId}_${index}`;
|
||||||
|
}
|
||||||
|
return row.id || row.batch_no || row.product_id || row.dispensing_id || `row-${index}`;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+12
-23
@@ -4,6 +4,7 @@ import { createColumnHelper } from '@tanstack/react-table';
|
|||||||
import { Table } from '../components/Table';
|
import { Table } from '../components/Table';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
import { CustomSelect } from '../components/CustomSelect';
|
import { CustomSelect } from '../components/CustomSelect';
|
||||||
|
import { StockDropdown } from '../components/StockDropdown';
|
||||||
import { getExpiringInventory, getLowStockInventory, authenticate, addStock, getDrugs, addMedicine, adjustStock } from '../services/api';
|
import { getExpiringInventory, getLowStockInventory, authenticate, addStock, getDrugs, addMedicine, adjustStock } from '../services/api';
|
||||||
|
|
||||||
interface InventoryBatch {
|
interface InventoryBatch {
|
||||||
@@ -450,7 +451,7 @@ const Inventory = () => {
|
|||||||
<Table
|
<Table
|
||||||
data={currentData}
|
data={currentData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
getRowId={(item) => item.batch_no}
|
getRowId={(item, index) => item.batch_no ? `${item.batch_no}_${item.product_id || index}` : `inv-${index}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -503,28 +504,16 @@ const Inventory = () => {
|
|||||||
<div className="grid grid-cols-2 overflow-y-auto" style={{ padding: '24px', gap: '24px' }}>
|
<div className="grid grid-cols-2 overflow-y-auto" style={{ padding: '24px', gap: '24px' }}>
|
||||||
{stockMode === 'restock' ? (
|
{stockMode === 'restock' ? (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Product ID</label>
|
<StockDropdown
|
||||||
<div className="relative group">
|
label="Product / Medicine (Search by Brand, Generic or Product ID)"
|
||||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-blue-500 transition-colors z-10">
|
placeholder="Search or select medicine to restock..."
|
||||||
<Package size={18} strokeWidth={2} />
|
value={formData.product_id}
|
||||||
</div>
|
options={drugOptions}
|
||||||
<input
|
mode="product"
|
||||||
type="text"
|
onChange={(val) => {
|
||||||
list="product-options"
|
setFormData({ ...formData, product_id: val });
|
||||||
value={formData.product_id}
|
}}
|
||||||
onChange={(e) => setFormData({ ...formData, product_id: e.target.value })}
|
/>
|
||||||
placeholder="Select or enter Product ID..."
|
|
||||||
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' }}
|
|
||||||
/>
|
|
||||||
<datalist id="product-options">
|
|
||||||
{drugOptions.map(d => (
|
|
||||||
<option key={d.product_id} value={d.product_id}>
|
|
||||||
{d.brand_name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+30
-19
@@ -4,6 +4,7 @@ import { createColumnHelper } from '@tanstack/react-table';
|
|||||||
import { Table } from '../components/Table';
|
import { Table } from '../components/Table';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
import { CustomSelect } from '../components/CustomSelect';
|
import { CustomSelect } from '../components/CustomSelect';
|
||||||
|
import { StockDropdown } from '../components/StockDropdown';
|
||||||
import { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder, downloadInvoice, fetchSyncedPatients } from '../services/api';
|
import { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder, downloadInvoice, fetchSyncedPatients } from '../services/api';
|
||||||
|
|
||||||
interface PharmacyItem {
|
interface PharmacyItem {
|
||||||
@@ -290,8 +291,8 @@ const Pharmacy = () => {
|
|||||||
|
|
||||||
{/* Metrics Row */}
|
{/* Metrics Row */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0" style={{ marginBottom: '32px' }}>
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0" style={{ marginBottom: '32px' }}>
|
||||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex items-center gap-5">
|
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex items-center gap-5" style={{ padding: "24px" }}>
|
||||||
<div className="p-4 rounded-2xl bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200/50 flex items-center justify-center shrink-0 shadow-sm">
|
<div className="p-4 rounded-2xl bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200/50 flex items-center justify-center shrink-0 shadow-sm" style={{ padding: "16px" }}>
|
||||||
<Package className="text-blue-600" size={28} />
|
<Package className="text-blue-600" size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -304,7 +305,7 @@ const Pharmacy = () => {
|
|||||||
{lowStockCount > 0 && (
|
{lowStockCount > 0 && (
|
||||||
<div className="absolute top-0 right-0 w-2 h-full bg-red-500"></div>
|
<div className="absolute top-0 right-0 w-2 h-full bg-red-500"></div>
|
||||||
)}
|
)}
|
||||||
<div className="p-4 rounded-2xl bg-gradient-to-br from-red-50 to-red-100 border border-red-200/50 flex items-center justify-center shrink-0 shadow-sm">
|
<div className="p-4 rounded-2xl bg-gradient-to-br from-red-50 to-red-100 border border-red-200/50 flex items-center justify-center shrink-0 shadow-sm" style={{ padding: "16px" }}>
|
||||||
<AlertCircle className="text-red-600" size={28} />
|
<AlertCircle className="text-red-600" size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -313,8 +314,8 @@ const Pharmacy = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex items-center gap-5">
|
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex items-center gap-5" style={{ padding: "24px" }}>
|
||||||
<div className="p-4 rounded-2xl bg-gradient-to-br from-indigo-50 to-indigo-100 border border-indigo-200/50 flex items-center justify-center shrink-0 shadow-sm">
|
<div className="p-4 rounded-2xl bg-gradient-to-br from-indigo-50 to-indigo-100 border border-indigo-200/50 flex items-center justify-center shrink-0 shadow-sm" style={{ padding: "16px" }}>
|
||||||
<Layers className="text-indigo-600" size={28} />
|
<Layers className="text-indigo-600" size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -521,22 +522,32 @@ const Pharmacy = () => {
|
|||||||
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Select Medicines (from inventory)</label>
|
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Select Medicines (from inventory)</label>
|
||||||
<CustomSelect
|
<StockDropdown
|
||||||
value=""
|
mode="product"
|
||||||
onChange={(e) => {
|
placeholder="Search or select a medicine to add..."
|
||||||
const item = inventory.find(i => i.id === e.target.value);
|
options={inventory.map(i => ({
|
||||||
if (item && !selectedItems.find(i => i.product_id === item.id)) {
|
id: i.id,
|
||||||
setSelectedItems([...selectedItems, { product_id: item.id, name: item.medicine, qty: 1, price: item.rawPrice }]);
|
product_id: i.id,
|
||||||
|
brand_name: i.medicine,
|
||||||
|
generic_name: i.subtitle,
|
||||||
|
qty_on_hand: i.stock,
|
||||||
|
mrp: i.rawPrice,
|
||||||
|
manufacturer: i.manufacture
|
||||||
|
}))}
|
||||||
|
onChange={(val, item) => {
|
||||||
|
if (item && val && !selectedItems.find(i => i.product_id === val)) {
|
||||||
|
setSelectedItems([
|
||||||
|
...selectedItems,
|
||||||
|
{
|
||||||
|
product_id: item.product_id || item.id || val,
|
||||||
|
name: item.brand_name || '',
|
||||||
|
qty: 1,
|
||||||
|
price: Number(item.mrp || 0)
|
||||||
|
}
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<option value="" disabled>Select a medicine to add...</option>
|
|
||||||
{inventory.filter(i => i.stock > 0).map(item => (
|
|
||||||
<option key={item.id} value={item.id}>
|
|
||||||
{item.medicine} (Stock: {item.stock})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedItems.length > 0 && (
|
{selectedItems.length > 0 && (
|
||||||
|
|||||||
+20
-18
@@ -349,8 +349,8 @@ const Prescriptions = () => {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
|
||||||
|
|
||||||
{/* Upload Column */}
|
{/* Upload Column */}
|
||||||
<div className="bg-white border border-slate-200 shadow-sm rounded-2xl p-8 h-fit">
|
<div className="flex flex-col h-fit">
|
||||||
<h2 className="mb-6 text-2xl font-bold text-slate-800">Upload Scan</h2>
|
<h2 className="mb-4 text-xl font-bold text-slate-800">Upload Scan</h2>
|
||||||
|
|
||||||
<div className="border-2 border-dashed border-blue-500/30 rounded-2xl p-10 flex flex-col items-center justify-center text-center bg-blue-50 hover:bg-blue-100/50 transition-colors">
|
<div className="border-2 border-dashed border-blue-500/30 rounded-2xl p-10 flex flex-col items-center justify-center text-center bg-blue-50 hover:bg-blue-100/50 transition-colors">
|
||||||
<UploadCloud size={48} className="text-blue-400 mb-4" />
|
<UploadCloud size={48} className="text-blue-400 mb-4" />
|
||||||
@@ -403,7 +403,7 @@ const Prescriptions = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results Column */}
|
{/* Results Column */}
|
||||||
<div className="bg-white border border-slate-200 shadow-sm rounded-2xl p-6">
|
<div className="flex flex-col">
|
||||||
<h2 className="mb-4 text-xl font-bold text-slate-800">Extraction Results</h2>
|
<h2 className="mb-4 text-xl font-bold text-slate-800">Extraction Results</h2>
|
||||||
|
|
||||||
{(isUploading || isPolling) && !prescriptionData && (
|
{(isUploading || isPolling) && !prescriptionData && (
|
||||||
@@ -423,9 +423,10 @@ const Prescriptions = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{prescriptionData && (
|
{prescriptionData && (
|
||||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<>
|
||||||
|
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500 bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
|
||||||
|
|
||||||
<div className="p-4 bg-emerald-50 border border-emerald-100 rounded-xl flex items-start gap-3">
|
<div className="px-6 py-4 bg-emerald-50 border-b border-emerald-100 flex items-start gap-3">
|
||||||
<CheckCircle className="text-emerald-500 mt-0.5" size={20} />
|
<CheckCircle className="text-emerald-500 mt-0.5" size={20} />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-emerald-800">Extraction Complete</p>
|
<p className="font-bold text-emerald-800">Extraction Complete</p>
|
||||||
@@ -433,8 +434,8 @@ const Prescriptions = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-6 w-full">
|
<div className="flex flex-col w-full px-6 py-6 gap-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
<label className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||||
<User size={16} className="text-slate-400" /> Patient Name
|
<User size={16} className="text-slate-400" /> Patient Name
|
||||||
@@ -450,7 +451,7 @@ const Prescriptions = () => {
|
|||||||
patient_name: e.target.value
|
patient_name: e.target.value
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
className="w-full px-4 py-2.5 bg-white border border-slate-300 rounded-lg text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -469,13 +470,13 @@ const Prescriptions = () => {
|
|||||||
doctor_name: e.target.value
|
doctor_name: e.target.value
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
className="w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
className="w-full px-4 py-2.5 bg-white border border-slate-300 rounded-lg text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
<div className="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
|
||||||
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
|
<div className="flex items-center gap-3 px-5 py-4 border-b border-slate-100 bg-slate-50/50">
|
||||||
<Pill className="text-blue-500" size={20} />
|
<Pill className="text-blue-500" size={20} />
|
||||||
<h3 className="font-semibold text-slate-800 text-lg">Prescribed Medications</h3>
|
<h3 className="font-semibold text-slate-800 text-lg">Prescribed Medications</h3>
|
||||||
<span className="ml-auto bg-slate-100 text-slate-600 py-1 px-3 rounded-full text-xs font-semibold">
|
<span className="ml-auto bg-slate-100 text-slate-600 py-1 px-3 rounded-full text-xs font-semibold">
|
||||||
@@ -509,7 +510,7 @@ const Prescriptions = () => {
|
|||||||
<select
|
<select
|
||||||
value={mappedItems[idx] || ''}
|
value={mappedItems[idx] || ''}
|
||||||
onChange={(e) => setMappedItems({...mappedItems, [idx]: e.target.value})}
|
onChange={(e) => setMappedItems({...mappedItems, [idx]: e.target.value})}
|
||||||
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-md text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-4 py-3 bg-white border border-slate-300 rounded-md text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="">-- Select Product --</option>
|
<option value="">-- Select Product --</option>
|
||||||
{drugOptions.map(d => {
|
{drugOptions.map(d => {
|
||||||
@@ -544,7 +545,7 @@ const Prescriptions = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-6 border-t border-slate-100 flex justify-end">
|
<div className="pt-4 flex justify-end border-t border-slate-100 mt-2">
|
||||||
{isConfirmed ? (
|
{isConfirmed ? (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button disabled className="bg-emerald-100 text-emerald-700 font-bold rounded-xl flex items-center gap-2" style={{ padding: '10px 16px' }}>
|
<button disabled className="bg-emerald-100 text-emerald-700 font-bold rounded-xl flex items-center gap-2" style={{ padding: '10px 16px' }}>
|
||||||
@@ -573,8 +574,9 @@ const Prescriptions = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{precheckResult && !dispenseSuccess && (
|
|
||||||
|
{precheckResult && !dispenseSuccess && (
|
||||||
<div className="mt-6 p-4 bg-slate-50 border border-slate-200 rounded-xl animate-in fade-in">
|
<div className="mt-6 p-4 bg-slate-50 border border-slate-200 rounded-xl animate-in fade-in">
|
||||||
<h3 className="font-bold text-slate-800 mb-2">Safety Pre-Check Results</h3>
|
<h3 className="font-bold text-slate-800 mb-2">Safety Pre-Check Results</h3>
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
@@ -602,7 +604,7 @@ const Prescriptions = () => {
|
|||||||
<select
|
<select
|
||||||
value={paymentMethod}
|
value={paymentMethod}
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||||
className="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="cash">Cash</option>
|
<option value="cash">Cash</option>
|
||||||
<option value="upi">UPI / Scanner</option>
|
<option value="upi">UPI / Scanner</option>
|
||||||
@@ -631,7 +633,7 @@ const Prescriptions = () => {
|
|||||||
<p className="text-emerald-600 mt-1">Inventory has been deducted and audit logs updated.</p>
|
<p className="text-emerald-600 mt-1">Inventory has been deducted and audit logs updated.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -651,7 +653,7 @@ const Prescriptions = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto p-0">
|
<div className="overflow-x-auto">
|
||||||
{prescriptionsList.length === 0 ? (
|
{prescriptionsList.length === 0 ? (
|
||||||
<div className="p-16 text-center text-slate-500">
|
<div className="p-16 text-center text-slate-500">
|
||||||
<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
||||||
|
|||||||
Reference in New Issue
Block a user