first commit .
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
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 { getDrugs, authenticate, addMedicine, createOrderDraft, updateOrderItems, precheckOrder, dispenseOrder } 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: 'Price',
|
||||
size: 120,
|
||||
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 [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: '',
|
||||
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 resetDispenseModal = () => {
|
||||
setIsDispenseModalOpen(false);
|
||||
setDispenseStep(1);
|
||||
setOrderDraftId(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);
|
||||
await dispenseOrder(orderDraftId, {
|
||||
subject_ref: patientName,
|
||||
prescriber_name: prescriberName,
|
||||
payment_method: 'cash'
|
||||
});
|
||||
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: '', 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) => ({
|
||||
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: parseFloat(drug.mrp) || 0,
|
||||
price: new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(parseFloat(drug.mrp) || 0),
|
||||
manufacture: drug.manufacturer || 'Unknown',
|
||||
type: drug.form || 'Unknown'
|
||||
}));
|
||||
setInventory(mappedData);
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
if (!localStorage.getItem('auth_token')) {
|
||||
await authenticate();
|
||||
}
|
||||
|
||||
const data = await getDrugs();
|
||||
if (data && data.drugs) {
|
||||
mapAndSetData(data.drugs);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
try {
|
||||
await authenticate();
|
||||
const retryData = await getDrugs();
|
||||
if (retryData && retryData.drugs) {
|
||||
mapAndSetData(retryData.drugs);
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error("Failed to fetch drugs after retry:", retryError);
|
||||
setError("Authentication failed. Please try logging in again.");
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch drugs:", error);
|
||||
setError("Failed to connect to the server. Please try again later.");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Filter inventory based on search query
|
||||
const filteredInventory = useMemo(() => {
|
||||
return inventory.filter(item =>
|
||||
item.medicine.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.subtitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.manufacture.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [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">
|
||||
<header className="mb-6 shrink-0 flex items-center justify-between">
|
||||
<div>
|
||||
<h1>Pharmacy Overview</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Metrics Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0" style={{ marginBottom: '32px' }}>
|
||||
<div className="bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center shrink-0">
|
||||
<Package className="text-blue-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 text-[13px] font-medium mb-1">Total Medicines</p>
|
||||
<h3 className="text-2xl font-bold text-slate-900">{inventory.length}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center shrink-0">
|
||||
<AlertCircle className="text-red-500" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 text-[13px] font-medium mb-1">Low Stock Alerts</p>
|
||||
<h3 className="text-2xl font-bold text-slate-900">{lowStockCount}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center shrink-0">
|
||||
<Layers className="text-indigo-500" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 text-[13px] font-medium mb-1">Unique Categories</p>
|
||||
<h3 className="text-2xl font-bold 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 max-w-2xl 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={{ paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsDispenseModalOpen(true)}
|
||||
className="flex items-center gap-2 bg-emerald-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-emerald-700 transition-colors shadow-sm whitespace-nowrap"
|
||||
>
|
||||
<Package size={18} strokeWidth={2.5} />
|
||||
Manual Dispense
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="flex items-center gap-2 bg-blue-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-blue-700 transition-colors shadow-sm whitespace-nowrap"
|
||||
>
|
||||
<Plus size={18} strokeWidth={2.5} />
|
||||
Add Medicine
|
||||
</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>
|
||||
|
||||
<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</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patientName}
|
||||
onChange={e => setPatientName(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. John Doe"
|
||||
/>
|
||||
</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>
|
||||
<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 }]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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 && (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pharmacy;
|
||||
Reference in New Issue
Block a user