Files
AI_Pharmacy_Frontend/src/pages/Pharmacy.tsx
T

720 lines
34 KiB
TypeScript

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';
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 {
id: string;
medicine: string;
subtitle: string;
badge: string;
stock: number;
rawPrice: number;
price: string;
manufacture: string;
type: string;
}
const columnHelper = createColumnHelper<PharmacyItem>();
const columns = [
columnHelper.accessor('medicine', {
header: 'Medicine',
size: 350,
cell: info => {
const item = info.row.original;
return (
<div className="py-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-800 text-[14px]">{item.medicine}</span>
{item.badge && (
<span className="bg-blue-600 text-white text-[10px] px-2 py-0.5 rounded font-semibold uppercase tracking-wide">
{item.badge}
</span>
)}
</div>
<span className="text-[13px] text-slate-400 block mt-1">{item.subtitle}</span>
</div>
);
}
}),
columnHelper.accessor('stock', {
header: 'Stock',
size: 100,
cell: info => {
const stock = info.getValue();
const isLow = stock < 50;
return (
<span className={`font-medium text-[14px] ${isLow ? 'text-red-500' : 'text-slate-800'}`}>
{stock}
</span>
);
}
}),
columnHelper.accessor('price', {
header: 'Purchase Price / MRP',
size: 150,
cell: info => <span className="text-slate-800 font-medium text-[14px]">{info.getValue()}</span>
}),
columnHelper.accessor('manufacture', {
header: 'Manufacturer',
size: 180,
cell: info => <span className="text-slate-600 text-[14px]">{info.getValue()}</span>
}),
columnHelper.accessor('type', {
header: 'Type',
size: 100,
cell: info => <span className="text-slate-600 text-[14px] capitalize">{info.getValue()}</span>
}),
];
const Pharmacy = () => {
const location = useLocation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [inventory, setInventory] = useState<PharmacyItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// Add Medicine Modal State
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isAdding, setIsAdding] = useState(false);
const [addFormData, setAddFormData] = useState({
brand_name: '',
generic_name: '',
manufacturer: '',
form: 'tablet',
strength: '',
mrp: '',
pack_size: '1',
schedule: 'H'
});
// Dispense Modal State
const [isDispenseModalOpen, setIsDispenseModalOpen] = useState(false);
const [dispenseStep, setDispenseStep] = useState(1); // 1: items, 2: precheck, 3: success
const [orderDraftId, setOrderDraftId] = useState<string | null>(null);
const [patientName, setPatientName] = useState('');
const [prescriberName, setPrescriberName] = useState('');
const [selectedItems, setSelectedItems] = useState<{product_id: string, name: string, qty: number, price: number}[]>([]);
const [precheckResult, setPrecheckResult] = useState<any>(null);
const [isProcessingDispense, setIsProcessingDispense] = useState(false);
const [dispensingId, setDispensingId] = useState<string | null>(null);
const resetDispenseModal = () => {
setIsDispenseModalOpen(false);
setDispenseStep(1);
setOrderDraftId(null);
setDispensingId(null);
setPatientName('');
setSelectedItems([]);
setPrecheckResult(null);
};
const handleCreateDraft = async () => {
if (!patientName || selectedItems.length === 0) return alert("Enter patient name and select items.");
try {
setIsProcessingDispense(true);
const draft = await createOrderDraft({ subject_ref: patientName });
setOrderDraftId(draft.order_draft_id);
const itemsPayload = selectedItems.map(i => ({ product_id: i.product_id, qty: i.qty }));
await updateOrderItems(draft.order_draft_id, itemsPayload);
const precheck = await precheckOrder(draft.order_draft_id, {
clinical_context: { patient_id: patientName, allergies: [] },
items: itemsPayload
});
setPrecheckResult(precheck);
setDispenseStep(2);
} catch (e) {
console.error(e);
alert("Failed to prepare order.");
} finally {
setIsProcessingDispense(false);
}
};
const handleExecuteDispense = async () => {
if (!orderDraftId) return;
try {
setIsProcessingDispense(true);
const res = await dispenseOrder(orderDraftId, {
subject_ref: patientName,
prescriber_name: prescriberName,
payment_method: 'cash'
});
setDispensingId(res.dispensing_id);
setDispenseStep(3);
loadData(); // refresh inventory
} catch (e: any) {
console.error(e);
alert("Dispense failed: " + (e.response?.data?.message || "Check stock."));
} finally {
setIsProcessingDispense(false);
}
};
const handleAddSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setIsAdding(true);
await addMedicine(addFormData);
setIsAddModalOpen(false);
setAddFormData({ brand_name: '', generic_name: '', manufacturer: '', form: 'tablet', strength: '', mrp: '', pack_size: '1', schedule: 'H' });
await loadData();
} catch (err) {
console.error("Failed to add medicine", err);
alert("Failed to add medicine. Please try again.");
} finally {
setIsAdding(false);
}
};
const mapAndSetData = (drugs: any[]) => {
const mappedData: PharmacyItem[] = drugs.map((drug: any) => {
const mrp = parseFloat(drug.mrp) || 0;
const packSize = parseInt(drug.pack_size, 10) || 1;
const unitPrice = mrp / packSize;
return {
id: drug.product_id,
medicine: `${drug.brand_name} ${drug.strength || ''}`.trim(),
subtitle: drug.generic_name || '',
badge: drug.schedule || '',
stock: parseInt(drug.total_stock) || 0,
rawPrice: unitPrice,
price: new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(unitPrice),
manufacture: drug.manufacturer || 'Unknown',
type: drug.form || 'Unknown'
};
});
setInventory(mappedData);
};
const [hospitalPatients, setHospitalPatients] = useState<any[]>([]);
const loadData = async () => {
try {
setIsLoading(true);
setError(null);
if (!localStorage.getItem('auth_token')) {
await authenticate();
}
const [data, patients] = await Promise.all([
getDrugs(),
fetchSyncedPatients().catch(() => []) // fail gracefully if hospital api is down
]);
if (data && data.drugs) {
mapAndSetData(data.drugs);
}
if (patients) {
setHospitalPatients(patients);
}
} catch (error: any) {
if (error.response && error.response.status === 401) {
try {
await authenticate();
const retryData = await getDrugs();
if (retryData && retryData.drugs) {
mapAndSetData(retryData.drugs);
}
const retryPatients = await fetchSyncedPatients().catch(() => []);
setHospitalPatients(retryPatients);
} catch (retryError) {
console.error("Failed to fetch data after retry:", retryError);
setError("Authentication failed. Please try logging in again.");
}
} else {
console.error("Failed to fetch data:", error);
setError("Failed to connect to the server. Please try again later.");
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadData();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('action') === 'dispense') {
setIsDispenseModalOpen(true);
window.history.replaceState({}, '', '/pharmacy');
}
}, []);
// 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(() => {
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
useEffect(() => {
setCurrentPage(1);
}, [searchQuery]);
const totalItems = filteredInventory.length;
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
const currentData = filteredInventory.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
// Business Metrics
const lowStockCount = inventory.filter(item => item.stock < 50).length;
const uniqueCategories = new Set(inventory.map(item => item.type)).size;
return (
<div className="max-w-[1400px] mx-auto flex flex-col w-full">
<div className="flex flex-col mb-8 gap-2">
<div className="flex items-center gap-4">
<div className="bg-blue-100 p-3 rounded-xl flex items-center justify-center">
<Package className="text-blue-600" size={26} />
</div>
<h1 className="text-3xl font-bold text-slate-900 m-0 leading-none">Pharmacy Overview</h1>
</div>
<p className="text-slate-500 mt-2">Manage your pharmacy inventory, add medicines, and handle manual dispensing.</p>
</div>
{/* 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" 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">
<p className="text-slate-500 text-sm font-semibold tracking-wide uppercase mb-1">Total Medicines</p>
<h3 className="text-3xl font-extrabold text-slate-900">{inventory.length}</h3>
</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 relative overflow-hidden">
{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" style={{ padding: "16px" }}>
<AlertCircle className="text-red-600" size={28} />
</div>
<div className="flex flex-col">
<p className="text-slate-500 text-sm font-semibold tracking-wide uppercase mb-1">Low Stock Alerts</p>
<h3 className={`text-3xl font-extrabold ${lowStockCount > 0 ? 'text-red-600' : 'text-slate-900'}`}>{lowStockCount}</h3>
</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" 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">
<p className="text-slate-500 text-sm font-semibold tracking-wide uppercase mb-1">Unique Categories</p>
<h3 className="text-3xl font-extrabold text-slate-900">{uniqueCategories}</h3>
</div>
</div>
</div>
{/* Action Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4" style={{ marginBottom: '40px', marginTop: '16px' }}>
<div className="relative w-full flex-1 max-w-none group">
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none transition-colors text-slate-400 group-focus-within:text-blue-500" style={{ paddingLeft: '16px' }}>
<Search size={20} strokeWidth={2} />
</div>
<input
type="text"
value={searchQuery}
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: 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>
</div>
<div className="w-full flex flex-col bg-white border border-slate-200 rounded-2xl shadow-sm relative" style={{ marginTop: '16px' }}>
<div className="overflow-x-auto">
{error ? (
<div className="p-8 text-center text-red-500">
<AlertCircle className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>{error}</p>
<button
onClick={loadData}
className="mt-4 px-4 py-2 bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors"
>
Try Again
</button>
</div>
) : isLoading ? (
<div className="p-8 flex flex-col items-center justify-center min-h-[400px]">
<div className="w-10 h-10 border-4 border-slate-200 border-t-blue-600 rounded-full animate-spin mb-4"></div>
<p className="text-slate-500">Loading pharmacy records...</p>
</div>
) : inventory.length === 0 ? (
<div className="p-16 text-center text-slate-500 flex flex-col items-center min-h-[400px] justify-center">
<Package className="w-16 h-16 mx-auto mb-6 opacity-20" />
<h3 className="text-xl font-medium text-slate-700 mb-2">No Medicines Found</h3>
<p className="max-w-md mx-auto">Your pharmacy inventory is currently empty or no items match your search.</p>
<button
onClick={loadData}
className="mt-6 px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
>
Refresh Inventory
</button>
</div>
) : (
<Table
data={currentData}
columns={columns}
getRowId={(item) => item.id}
/>
)}
</div>
{totalItems > 0 && !isLoading && !error && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={[10, 25, 50, 100]}
onPageChange={(page) => setCurrentPage(page)}
onItemsPerPageChange={(limit) => {
setItemsPerPage(limit);
setCurrentPage(1);
}}
/>
)}
</div>
{/* Add Medicine Modal */}
{isAddModalOpen && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between border-b border-slate-100 bg-slate-50/50" style={{ padding: '24px' }}>
<h2 className="text-xl font-bold text-slate-800">Add New Medicine to Catalogue</h2>
<button
onClick={() => setIsAddModalOpen(false)}
className="text-slate-400 hover:text-slate-600 transition-colors p-2 hover:bg-slate-100 rounded-full"
>
<X size={20} />
</button>
</div>
<form onSubmit={handleAddSubmit} style={{ padding: '24px' }}>
<div className="grid grid-cols-2" style={{ gap: '24px', marginBottom: '24px' }}>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Brand Name</label>
<input required type="text" value={addFormData.brand_name} onChange={e => setAddFormData({...addFormData, brand_name: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="e.g. Calpol 500" style={{ padding: '12px 16px' }} />
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Generic Name</label>
<input required type="text" value={addFormData.generic_name} onChange={e => setAddFormData({...addFormData, generic_name: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="e.g. Paracetamol" style={{ padding: '12px 16px' }} />
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Manufacturer</label>
<input type="text" value={addFormData.manufacturer} onChange={e => setAddFormData({...addFormData, manufacturer: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="e.g. GSK" style={{ padding: '12px 16px' }} />
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Form</label>
<CustomSelect value={addFormData.form} onChange={e => setAddFormData({...addFormData, form: e.target.value})}>
<option value="tablet">Tablet</option>
<option value="capsule">Capsule</option>
<option value="syrup">Syrup</option>
<option value="injection">Injection</option>
<option value="ointment">Ointment</option>
</CustomSelect>
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Strength</label>
<input type="text" value={addFormData.strength} onChange={e => setAddFormData({...addFormData, strength: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="e.g. 500mg" style={{ padding: '12px 16px' }} />
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>MRP (₹)</label>
<input required type="number" step="0.01" value={addFormData.mrp} onChange={e => setAddFormData({...addFormData, mrp: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="0.00" style={{ padding: '12px 16px' }} />
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Pack Size</label>
<input type="number" value={addFormData.pack_size} onChange={e => setAddFormData({...addFormData, pack_size: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500" placeholder="1" style={{ padding: '12px 16px' }} />
</div>
</div>
<div className="flex items-center justify-end border-t border-slate-100" style={{ paddingTop: '16px', gap: '12px' }}>
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="text-slate-600 font-medium hover:bg-slate-100 rounded-xl transition-colors"
style={{ padding: '10px 20px' }}
>
Cancel
</button>
<button
type="submit"
disabled={isAdding}
className="bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 transition-colors shadow-sm disabled:opacity-70 disabled:cursor-not-allowed flex items-center"
style={{ padding: '10px 20px', gap: '8px' }}
>
{isAdding ? 'Adding...' : 'Add Medicine'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Dispense Modal */}
{isDispenseModalOpen && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between border-b border-slate-100 bg-slate-50/50" style={{ padding: '24px' }}>
<h2 className="text-xl font-bold text-slate-800">Manual Dispense</h2>
<button
onClick={resetDispenseModal}
className="text-slate-400 hover:text-slate-600 transition-colors p-2 hover:bg-slate-100 rounded-full"
>
<X size={20} />
</button>
</div>
<div style={{ padding: '24px' }}>
{dispenseStep === 1 && (
<div>
<div className="grid grid-cols-1 sm:grid-cols-2" style={{ gap: '16px', marginBottom: '16px' }}>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Patient Name (from Hospital)</label>
<input
type="text"
value={patientName}
onChange={e => setPatientName(e.target.value)}
list="patients-list"
className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500"
style={{ padding: '12px 16px' }}
placeholder="e.g. John Doe"
/>
<datalist id="patients-list">
{hospitalPatients.map((p, idx) => (
<option key={idx} value={p.full_name}>{p.patient_code}</option>
))}
</datalist>
</div>
<div>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Prescriber Name</label>
<input
type="text"
value={prescriberName}
onChange={e => setPrescriberName(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500"
style={{ padding: '12px 16px' }}
placeholder="e.g. Dr. Smith"
/>
</div>
</div>
<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>
<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)
}
]);
}
}}
/>
</div>
{selectedItems.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Selected Items</label>
<div className="space-y-2">
{selectedItems.map((item, idx) => (
<div key={idx} className="flex items-center bg-white border border-slate-200 rounded-lg" style={{ gap: '16px', padding: '12px' }}>
<span className="flex-1 font-medium">{item.name}</span>
<input
type="number"
min="1"
value={item.qty}
onChange={(e) => {
const newItems = [...selectedItems];
newItems[idx].qty = parseInt(e.target.value) || 1;
setSelectedItems(newItems);
}}
className="w-20 border border-slate-200 rounded-md"
style={{ padding: '4px 8px' }}
/>
<button className="text-red-500" onClick={() => setSelectedItems(selectedItems.filter((_, i) => i !== idx))}>
<X size={16} />
</button>
</div>
))}
</div>
</div>
)}
<div className="flex justify-end border-t border-slate-100" style={{ paddingTop: '16px' }}>
<button
onClick={handleCreateDraft}
disabled={isProcessingDispense}
className="bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 transition-colors shadow-sm disabled:opacity-70"
style={{ padding: '10px 24px' }}
>
{isProcessingDispense ? 'Preparing...' : 'Run Precheck'}
</button>
</div>
</div>
)}
{dispenseStep === 2 && precheckResult && (
<div>
<h3 className="font-bold text-slate-800" style={{ marginBottom: '8px' }}>Safety Pre-Check Results</h3>
<div className="space-y-2 bg-slate-50 rounded-xl border border-slate-200" style={{ marginBottom: '16px', padding: '16px' }}>
{precheckResult.warnings?.map((w: any, idx: number) => (
<div key={idx} className="flex items-center gap-2 text-sm text-amber-700 font-medium">
<AlertCircle size={16} /> {w.code} - {w.batch_no}
</div>
))}
{(!precheckResult.warnings || precheckResult.warnings.length === 0) && (
<div className="flex items-center gap-2 text-sm text-emerald-600 font-medium">
All checks passed. No warnings.
</div>
)}
</div>
<div className="flex justify-between items-center" style={{ marginBottom: '24px' }}>
<span className="font-bold text-slate-600">Total Bill Amount</span>
<span className="text-xl font-bold text-slate-900">₹{precheckResult.total_amount?.toFixed(2) || '0.00'}</span>
</div>
<div className="flex justify-end border-t border-slate-100" style={{ paddingTop: '16px' }}>
<button
onClick={handleExecuteDispense}
disabled={isProcessingDispense}
className="bg-emerald-600 text-white font-semibold rounded-xl hover:bg-emerald-700 transition-colors shadow-sm disabled:opacity-70 flex items-center gap-2"
style={{ padding: '10px 24px' }}
>
<Package size={18} />
{isProcessingDispense ? 'Dispensing...' : 'Confirm Dispense'}
</button>
</div>
</div>
)}
{dispenseStep === 3 && (
<div className="text-center" style={{ padding: '32px 0' }}>
<div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto" style={{ marginBottom: '16px' }}>
<Package className="text-emerald-600" size={32} />
</div>
<h3 className="font-bold text-emerald-800 text-xl" style={{ marginBottom: '8px' }}>Dispense Successful!</h3>
<p className="text-emerald-600" style={{ marginBottom: '24px' }}>Stock has been deducted and logged.</p>
<div className="flex justify-center gap-4">
<button
onClick={async () => {
try {
await downloadInvoice(dispensingId || orderDraftId!);
} catch (e) {
alert("Failed to download invoice");
console.error(e);
}
}}
className="bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 transition-colors flex items-center gap-2"
style={{ padding: '10px 24px' }}
>
Download Invoice
</button>
<button
onClick={resetDispenseModal}
className="bg-slate-800 text-white font-semibold rounded-xl hover:bg-slate-900 transition-colors"
style={{ padding: '10px 24px' }}
>
Close
</button>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default Pharmacy;