feat: update frontend components and page layouts

This commit is contained in:
2026-09-15 11:25:56 +05:30
parent d46f78bbd0
commit 85a3275268
8 changed files with 530 additions and 78 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
VITE_API_BASE_URL=http://localhost:8080/v1
VITE_API_KEY=915e46784e798f21759e52bd2a9d4cc2
VITE_API_KEY=DUMUqJrNeyuaPDSZSsMf91qjgwc5K3N7
+11 -11
View File
@@ -65,6 +65,16 @@
"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",
@@ -1409,7 +1419,6 @@
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -1420,7 +1429,6 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1567,7 +1575,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.44",
"caniuse-lite": "^1.0.30001806",
@@ -2541,7 +2548,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2568,7 +2574,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
@@ -2599,7 +2604,6 @@
"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"
}
@@ -2609,7 +2613,6 @@
"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"
},
@@ -2629,7 +2632,6 @@
"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"
@@ -2720,8 +2722,7 @@
"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
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -2928,7 +2929,6 @@
"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",
+4 -4
View File
@@ -79,18 +79,18 @@ export const Header = () => {
</button>
{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
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
</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
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
+444
View File
@@ -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 -2
View File
@@ -8,7 +8,7 @@ import { MoreHorizontal } from 'lucide-react';
export interface TableProps<T> {
data: T[];
columns: any[];
getRowId?: (row: T) => string;
getRowId?: (row: T, index?: number) => string;
emptyMessage?: string;
}
@@ -17,7 +17,13 @@ export const Table = <T,>({ data, columns, getRowId, emptyMessage = 'No data ava
data,
columns,
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 (
+12 -23
View File
@@ -4,6 +4,7 @@ import { createColumnHelper } from '@tanstack/react-table';
import { Table } from '../components/Table';
import Pagination from '../components/Pagination';
import { CustomSelect } from '../components/CustomSelect';
import { StockDropdown } from '../components/StockDropdown';
import { getExpiringInventory, getLowStockInventory, authenticate, addStock, getDrugs, addMedicine, adjustStock } from '../services/api';
interface InventoryBatch {
@@ -450,7 +451,7 @@ const Inventory = () => {
<Table
data={currentData}
columns={columns}
getRowId={(item) => item.batch_no}
getRowId={(item, index) => item.batch_no ? `${item.batch_no}_${item.product_id || index}` : `inv-${index}`}
/>
)}
</div>
@@ -503,28 +504,16 @@ const Inventory = () => {
<div className="grid grid-cols-2 overflow-y-auto" style={{ padding: '24px', gap: '24px' }}>
{stockMode === 'restock' ? (
<div className="col-span-2">
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Product ID</label>
<div className="relative group">
<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">
<Package size={18} strokeWidth={2} />
</div>
<input
type="text"
list="product-options"
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>
<StockDropdown
label="Product / Medicine (Search by Brand, Generic or Product ID)"
placeholder="Search or select medicine to restock..."
value={formData.product_id}
options={drugOptions}
mode="product"
onChange={(val) => {
setFormData({ ...formData, product_id: val });
}}
/>
</div>
) : (
<>
+30 -19
View File
@@ -4,6 +4,7 @@ import { createColumnHelper } from '@tanstack/react-table';
import { Table } from '../components/Table';
import Pagination from '../components/Pagination';
import { CustomSelect } from '../components/CustomSelect';
import { StockDropdown } from '../components/StockDropdown';
import { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder, downloadInvoice, fetchSyncedPatients } from '../services/api';
interface PharmacyItem {
@@ -290,8 +291,8 @@ const Pharmacy = () => {
{/* Metrics Row */}
<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="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="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" style={{ padding: "16px" }}>
<Package className="text-blue-600" size={28} />
</div>
<div className="flex flex-col">
@@ -304,7 +305,7 @@ const Pharmacy = () => {
{lowStockCount > 0 && (
<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} />
</div>
<div className="flex flex-col">
@@ -313,8 +314,8 @@ const Pharmacy = () => {
</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="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="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" style={{ padding: "16px" }}>
<Layers className="text-indigo-600" size={28} />
</div>
<div className="flex flex-col">
@@ -521,22 +522,32 @@ const Pharmacy = () => {
<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>
<CustomSelect
value=""
onChange={(e) => {
const item = inventory.find(i => i.id === e.target.value);
if (item && !selectedItems.find(i => i.product_id === item.id)) {
setSelectedItems([...selectedItems, { product_id: item.id, name: item.medicine, qty: 1, price: item.rawPrice }]);
<StockDropdown
mode="product"
placeholder="Search or select a medicine to add..."
options={inventory.map(i => ({
id: i.id,
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>
{selectedItems.length > 0 && (
+20 -18
View File
@@ -349,8 +349,8 @@ const Prescriptions = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
{/* Upload Column */}
<div className="bg-white border border-slate-200 shadow-sm rounded-2xl p-8 h-fit">
<h2 className="mb-6 text-2xl font-bold text-slate-800">Upload Scan</h2>
<div className="flex flex-col h-fit">
<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">
<UploadCloud size={48} className="text-blue-400 mb-4" />
@@ -403,7 +403,7 @@ const Prescriptions = () => {
</div>
{/* 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>
{(isUploading || isPolling) && !prescriptionData && (
@@ -423,9 +423,10 @@ const Prescriptions = () => {
)}
{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} />
<div>
<p className="font-bold text-emerald-800">Extraction Complete</p>
@@ -433,8 +434,8 @@ const Prescriptions = () => {
</div>
</div>
<div className="flex flex-col gap-6 w-full">
<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="flex flex-col w-full px-6 py-6 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 text-sm font-semibold text-slate-700">
<User size={16} className="text-slate-400" /> Patient Name
@@ -450,7 +451,7 @@ const Prescriptions = () => {
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>
@@ -469,13 +470,13 @@ const Prescriptions = () => {
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 className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
<div className="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
<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} />
<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">
@@ -509,7 +510,7 @@ const Prescriptions = () => {
<select
value={mappedItems[idx] || ''}
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>
{drugOptions.map(d => {
@@ -544,7 +545,7 @@ const Prescriptions = () => {
</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 ? (
<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' }}>
@@ -573,8 +574,9 @@ const Prescriptions = () => {
</button>
)}
</div>
{precheckResult && !dispenseSuccess && (
</div>
{precheckResult && !dispenseSuccess && (
<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>
<div className="space-y-2 mb-4">
@@ -602,7 +604,7 @@ const Prescriptions = () => {
<select
value={paymentMethod}
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="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>
</div>
)}
</div>
</>
)}
</div>
@@ -651,7 +653,7 @@ const Prescriptions = () => {
</button>
</div>
<div className="overflow-x-auto p-0">
<div className="overflow-x-auto">
{prescriptionsList.length === 0 ? (
<div className="p-16 text-center text-slate-500">
<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />