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(); const columns = [ columnHelper.accessor('medicine', { header: 'Medicine', size: 350, cell: info => { const item = info.row.original; return (
{item.medicine} {item.badge && ( {item.badge} )}
{item.subtitle}
); } }), columnHelper.accessor('stock', { header: 'Stock', size: 100, cell: info => { const stock = info.getValue(); const isLow = stock < 50; return ( {stock} ); } }), columnHelper.accessor('price', { header: 'Purchase Price / MRP', size: 150, cell: info => {info.getValue()} }), columnHelper.accessor('manufacture', { header: 'Manufacturer', size: 180, cell: info => {info.getValue()} }), columnHelper.accessor('type', { header: 'Type', size: 100, cell: info => {info.getValue()} }), ]; const Pharmacy = () => { const location = useLocation(); const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(25); const [inventory, setInventory] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(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(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(null); const [isProcessingDispense, setIsProcessingDispense] = useState(false); const [dispensingId, setDispensingId] = useState(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([]); 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 (

Pharmacy Overview

Manage your pharmacy inventory, add medicines, and handle manual dispensing.

{/* Metrics Row */}

Total Medicines

{inventory.length}

{lowStockCount > 0 && (
)}

Low Stock Alerts

0 ? 'text-red-600' : 'text-slate-900'}`}>{lowStockCount}

Unique Categories

{uniqueCategories}

{/* Action Bar */}
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 && ( )}
{error ? (

{error}

) : isLoading ? (

Loading pharmacy records...

) : inventory.length === 0 ? (

No Medicines Found

Your pharmacy inventory is currently empty or no items match your search.

) : ( item.id} /> )} {totalItems > 0 && !isLoading && !error && ( setCurrentPage(page)} onItemsPerPageChange={(limit) => { setItemsPerPage(limit); setCurrentPage(1); }} /> )} {/* Add Medicine Modal */} {isAddModalOpen && (

Add New Medicine to Catalogue

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' }} />
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' }} />
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' }} />
setAddFormData({...addFormData, form: e.target.value})}>
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' }} />
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' }} />
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' }} />
)} {/* Dispense Modal */} {isDispenseModalOpen && (

Manual Dispense

{dispenseStep === 1 && (
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" /> {hospitalPatients.map((p, idx) => ( ))}
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" />
({ 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) } ]); } }} />
{selectedItems.length > 0 && (
{selectedItems.map((item, idx) => (
{item.name} { 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' }} />
))}
)}
)} {dispenseStep === 2 && precheckResult && (

Safety Pre-Check Results

{precheckResult.warnings?.map((w: any, idx: number) => (
{w.code} - {w.batch_no}
))} {(!precheckResult.warnings || precheckResult.warnings.length === 0) && (
All checks passed. No warnings.
)}
Total Bill Amount ₹{precheckResult.total_amount?.toFixed(2) || '0.00'}
)} {dispenseStep === 3 && (

Dispense Successful!

Stock has been deducted and logged.

)}
)} ); }; export default Pharmacy;