feat(pharmacy): add right-side patient old records, last dispensed medicines, and 1-click re-dispense/return desk

This commit is contained in:
DevSoftTech Server 2026-09-10 09:23:59 +05:30
parent 261a69bf2e
commit 741269634e
3 changed files with 777 additions and 3 deletions

View File

@ -1661,6 +1661,7 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
$routes->get('medical-store/stores', 'MedicalStoreApi::stores');
$routes->post('medical-store/stores/save', 'MedicalStoreApi::saveStore');
$routes->get('medical-store/patient/search', 'MedicalStoreApi::searchPatient');
$routes->get('medical-store/patient/history', 'MedicalStoreApi::patientHistory');
$routes->get('medical-store/patient/prescription/(:segment)/(:num)', 'MedicalStoreApi::getPrescription/$1/$2');
$routes->get('medical-store/items/search', 'MedicalStoreApi::searchItems');
$routes->get('medical-store/items/search-master', 'MedicalStoreApi::searchMasterItems');

View File

@ -213,7 +213,174 @@ class MedicalStoreApi extends BaseController
return $this->response->setJSON(['status' => 1, 'patients' => $enriched]);
}
public function patientHistory()
{
$patientId = (int)($this->request->getGet('patient_id') ?? 0);
$uhid = trim($this->request->getGet('uhid') ?? '');
$mobile = trim($this->request->getGet('mobile') ?? '');
$storeId = (int)($this->request->getGet('store_id') ?? 0);
if ($patientId <= 0 && empty($uhid) && empty($mobile)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 0,
'message' => 'Please provide patient_id, uhid, or mobile number.'
]);
}
// Fetch sales history for this patient
$builder = $this->db->table('mst_sales s');
$builder->select('s.*, st.store_name, st.store_code');
$builder->join('mst_stores st', 'st.store_id = s.store_id', 'left');
$builder->groupStart();
if ($patientId > 0) {
$builder->orWhere('s.patient_id', $patientId);
}
if (!empty($uhid)) {
$builder->orWhere('s.uhid', $uhid);
}
if (!empty($mobile) && strlen($mobile) >= 10) {
$builder->orWhere('s.patient_mobile', $mobile);
}
$builder->groupEnd();
$builder->orderBy('s.sale_id', 'DESC');
$sales = $builder->limit(30)->get()->getResultArray();
$invoices = [];
$totalSpend = 0;
$uniqueMedicinesMap = [];
foreach ($sales as $sale) {
$totalSpend += (float)($sale['net_amount'] ?? 0);
// Fetch items for this sale
$items = $this->db->table('mst_sales_items si')
->select('si.*, i.item_name, i.generic_name, i.category, i.unit_pack, i.drug_schedule, i.manufacturer_name')
->join('mst_items i', 'i.item_id = si.item_id', 'left')
->where('si.sale_id', $sale['sale_id'])
->get()
->getResultArray();
$processedItems = [];
foreach ($items as $it) {
$returnedUnits = 0;
if (($it['item_type'] ?? 'SALE') === 'SALE') {
$retRow = $this->db->table('mst_sales_items')
->where('ref_sale_id', $sale['sale_id'])
->where('item_id', $it['item_id'])
->where('batch_id', $it['batch_id'])
->where('item_type', 'RETURN')
->selectSum('total_units', 'tot_ret')
->get()
->getRowArray();
$returnedUnits = (int)($retRow['tot_ret'] ?? 0);
}
$soldUnits = (int)($it['total_units'] ?? $it['qty']);
$remainingUnits = max(0, $soldUnits - $returnedUnits);
$unitsPerPack = max(1, (int)($it['units_per_pack'] ?? 1));
$processedItem = [
'sale_item_id' => (int)$it['sale_item_id'],
'sale_id' => (int)$it['sale_id'],
'item_id' => (int)$it['item_id'],
'item_name' => $it['item_name'] ?? 'Unknown Medicine',
'generic_name' => $it['generic_name'] ?? '',
'category' => $it['category'] ?? '',
'unit_pack' => $it['unit_pack'] ?? '',
'drug_schedule' => $it['drug_schedule'] ?? '',
'batch_id' => (int)$it['batch_id'],
'batch_no' => $it['batch_no'] ?? '',
'expiry_date' => $it['expiry_date'] ?? '',
'qty' => (int)$it['qty'],
'sell_unit' => $it['sell_unit'] ?? 'Strip',
'units_per_pack' => $unitsPerPack,
'total_units' => $soldUnits,
'already_returned_units' => $returnedUnits,
'remaining_units' => $remainingUnits,
'remaining_strips' => floor($remainingUnits / $unitsPerPack),
'remaining_loose' => $remainingUnits % $unitsPerPack,
'unit_mrp' => (float)($it['unit_mrp'] ?? 0),
'effective_unit_price' => round((float)($it['total_amount'] ?? 0) / max(1, $soldUnits), 2),
'gst_rate' => (float)($it['gst_rate'] ?? 0),
'total_amount' => (float)($it['total_amount'] ?? 0),
'item_type' => $it['item_type'] ?? 'SALE'
];
$processedItems[] = $processedItem;
if (($it['item_type'] ?? 'SALE') === 'SALE' && !empty($it['item_id'])) {
$mId = (int)$it['item_id'];
if (!isset($uniqueMedicinesMap[$mId])) {
$uniqueMedicinesMap[$mId] = [
'item_id' => $mId,
'item_name' => $it['item_name'] ?? '',
'generic_name' => $it['generic_name'] ?? '',
'category' => $it['category'] ?? '',
'unit_pack' => $it['unit_pack'] ?? '',
'last_batch_no' => $it['batch_no'] ?? '',
'last_dispensed' => $sale['sale_date'],
'last_invoice_no' => $sale['invoice_no'],
'last_qty' => (int)$it['qty'],
'last_sell_unit' => $it['sell_unit'] ?? 'Strip',
'times_dispensed' => 1
];
} else {
$uniqueMedicinesMap[$mId]['times_dispensed']++;
}
}
}
$invoices[] = [
'sale' => $sale,
'items' => $processedItems
];
}
$lastInvoice = $invoices[0] ?? null;
$lastDispensedItems = [];
if ($lastInvoice) {
$lastDispensedItems = array_values(array_filter($lastInvoice['items'], function($it) {
return ($it['item_type'] ?? 'SALE') === 'SALE';
}));
}
// Fetch OPD doctor prescriptions for this patient if patient_id > 0
$docPrescriptions = [];
if ($patientId > 0) {
try {
$docPrescriptions = $this->db->table('opd_prescription op')
->select('opp.id as presc_item_id, opp.med_id, opp.med_name, opp.med_type, opp.dosage, opp.dosage_freq_str, opp.no_of_days, opp.remark, opp.qty as presc_qty, op.id as presc_id, op.opd_id, op.date_opd_visit, opm.doc_name')
->join('opd_prescrption_prescribed opp', 'opp.opd_pre_id = op.id')
->join('opd_master opm', 'opm.opd_id = op.opd_id', 'left')
->where('op.p_id', $patientId)
->orderBy('op.id', 'DESC')
->limit(20)
->get()
->getResultArray();
} catch (\Exception $e) {
$docPrescriptions = [];
}
}
return $this->response->setJSON([
'status' => 1,
'patient_id' => $patientId,
'uhid' => $uhid,
'total_sales' => count($sales),
'total_spend' => round($totalSpend, 2),
'last_sale' => $lastInvoice ? $lastInvoice['sale'] : null,
'last_dispensed_items' => $lastDispensedItems,
'invoices' => $invoices,
'unique_medicines' => array_values($uniqueMedicinesMap),
'doctor_prescriptions' => $docPrescriptions
]);
}
public function getPrescription(string $encounterType, int $encounterId)
{
$medicines = [];
$encounterType = strtolower($encounterType);

View File

@ -310,6 +310,91 @@
font-weight: 700;
}
/* Patient Pharmacy History & Records Sidebar */
.patient-history-sidebar {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.07);
overflow: hidden;
display: flex;
flex-direction: column;
}
.patient-history-header {
padding: 12px 16px;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #ffffff;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.patient-history-tabs {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 6px 8px;
display: flex;
gap: 4px;
overflow-x: auto;
}
.patient-history-tab-btn {
font-size: 0.72rem;
font-weight: 700;
padding: 5px 8px;
border-radius: 6px;
border: 1px solid transparent;
background: transparent;
color: #64748b;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
gap: 4px;
}
.patient-history-tab-btn.active {
background: #ffffff;
color: #0284c7;
border-color: #cbd5e1;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.patient-history-tab-btn:hover:not(.active) {
background: #f1f5f9;
color: #1e293b;
}
.patient-history-body {
padding: 12px;
overflow-y: auto;
flex: 1;
max-height: calc(100vh - 220px);
}
.patient-history-body::-webkit-scrollbar {
width: 5px;
}
.patient-history-body::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.history-med-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
transition: all 0.15s ease;
}
.history-med-card:hover {
border-color: #38bdf8;
box-shadow: 0 3px 10px rgba(56, 189, 248, 0.12);
}
.history-med-card.last-given {
background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
border-color: #86efac;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
.search-results-dropdown {
position: absolute;
top: 100%;
@ -898,6 +983,13 @@ function PosCounter({ store, notify }) {
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const [isPrintOpen, setIsPrintOpen] = useState(false);
// Patient History & Old Records State (Right-Side Panel)
const [patientHistory, setPatientHistory] = useState(null);
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const [showHistoryPanel, setShowHistoryPanel] = useState(true);
const [historyActiveTab, setHistoryActiveTab] = useState('last_given'); // 'last_given', 'all_bills', 'prescriptions', 'summary'
const [expandedBillId, setExpandedBillId] = useState(null);
// Automatically preload latest completed sale for this store so "Last Bill" is immediately available
useEffect(() => {
if (store && store.store_id && !lastInvoice) {
@ -1208,10 +1300,15 @@ function PosCounter({ store, notify }) {
setPatientType('Walk-in');
setPrintFormat('thermal'); // Default walkin to Thermal
}
// Fetch patient pharmacy history & old records on the right side
fetchPatientHistory(pt.patient_id, pt.uhid, pt.mobile);
setShowHistoryPanel(true);
};
const resetPatient = () => {
setSelectedPatient(null);
setPatientHistory(null);
setPatientQuery('');
setPatientType('Walk-in');
setDoctorName('');
@ -1221,6 +1318,7 @@ function PosCounter({ store, notify }) {
setPrintFormat('thermal');
};
const viewAbdmBundle = async (saleId) => {
setIsLoadingFhir(true);
setShowFhirModal(true);
@ -1503,6 +1601,107 @@ function PosCounter({ store, notify }) {
setCart(cart.filter((_, i) => i !== idx));
};
const fetchPatientHistory = async (patientId, uhid, mobile) => {
if (!patientId && !uhid && !mobile) {
setPatientHistory(null);
return;
}
setIsLoadingHistory(true);
try {
const p = new URLSearchParams();
if (patientId) p.append('patient_id', patientId);
if (uhid) p.append('uhid', uhid);
if (mobile) p.append('mobile', mobile);
if (store?.store_id) p.append('store_id', store.store_id);
const res = await fetch(API_BASE + `patient/history?${p.toString()}`);
const data = await res.json();
if (data.status) {
setPatientHistory(data);
if (data.invoices && data.invoices.length > 0) {
setExpandedBillId(data.invoices[0].sale.sale_id);
}
} else {
setPatientHistory(null);
}
} catch (e) {
console.error('Error fetching patient history:', e);
setPatientHistory(null);
} finally {
setIsLoadingHistory(false);
}
};
const reDispenseMedicine = async (itemObj, defaultQty = 1) => {
try {
const searchTerm = itemObj.item_name || itemObj.med_name;
if (!searchTerm) return;
notify(`Finding current stock for "${searchTerm}"...`, 'info');
const res = await fetch(API_BASE + `items/search?store_id=${store.store_id}&q=${encodeURIComponent(searchTerm)}`);
const data = await res.json();
if (data.status && data.items && data.items.length > 0) {
let matched = null;
if (itemObj.item_id) {
matched = data.items.find(it => Number(it.item_id) === Number(itemObj.item_id));
}
if (!matched) matched = data.items[0];
if (matched && matched.fefo_batch) {
const qtyToAdd = defaultQty || (itemObj.qty ? Number(itemObj.qty) : 1);
addItemToCart(matched, matched.fefo_batch, qtyToAdd);
notify(`Added "${matched.item_name}" (Batch ${matched.fefo_batch.batch_no}) to cart!`, 'success');
} else if (matched) {
notify(`"${matched.item_name}" is currently OUT OF STOCK in this counter!`, 'error');
}
} else {
notify(`Medicine "${searchTerm}" not found in current inventory.`, 'error');
}
} catch (e) {
notify('Failed to add medicine: ' + e.message, 'error');
}
};
const initiateReturnFromHistory = (saleObj, itemObj) => {
if (!saleObj || !itemObj) return;
const inv = patientHistory?.invoices?.find(inv => Number(inv.sale.sale_id) === Number(saleObj.sale_id));
if (inv) {
selectReturnInvoice(inv);
const upp = Number(itemObj.units_per_pack) || 1;
const defUnit = (upp > 1 && itemObj.remaining_strips > 0) ? 'Strip' : 'Tab';
const defQty = defUnit === 'Strip' ? Math.max(1, itemObj.remaining_strips) : Math.max(1, itemObj.remaining_units);
setReturnDraftItems(prev => ({
...prev,
[itemObj.sale_item_id]: {
return_qty: defQty,
return_unit: defUnit,
condition: 'GOOD',
reason: 'EXCESS_UNUSED'
}
}));
openReturnModal();
notify(`Loaded bill ${saleObj.invoice_no} for return of ${itemObj.item_name}`, 'info');
} else {
setReturnSearchQuery(saleObj.invoice_no);
searchReturnInvoices(saleObj.invoice_no);
openReturnModal();
}
};
const viewPastBill = (saleId) => {
fetch(API_BASE + `sales/invoice/${saleId}`)
.then(r => r.json())
.then(data => {
if (data.status) {
setLastInvoice(data);
setIsPrintOpen(true);
} else {
notify('Unable to load invoice', 'error');
}
}).catch(err => notify('Error loading bill: ' + err.message, 'error'));
};
// Calculate totals with whole bill discount and return items
const grossTotal = cart.reduce((acc, it) => acc + getItemLineInfo(it).lineGross, 0);
const itemDiscountTotal = cart.reduce((acc, it) => acc + getItemDiscount(it), 0);
@ -2044,8 +2243,8 @@ function PosCounter({ store, notify }) {
return (
<div className="pos-counter-wrapper position-relative">
<div className="row g-3">
{/* Full-Width Workspace: Patient Selector, Medicine Lookup & Cart Grid */}
<div className="col-12">
{/* Workspace Column (col-lg-8 when patient history open, col-12 otherwise) */}
<div className={selectedPatient && showHistoryPanel ? "col-12 col-lg-8 col-xl-8" : "col-12"}>
{/* Patient Information Panel */}
<div className="counter-card position-relative">
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
@ -2067,7 +2266,22 @@ function PosCounter({ store, notify }) {
</button>
)}
{selectedPatient && (
<div className="d-flex gap-2">
<div className="d-flex gap-1.5 flex-wrap">
<button
type="button"
className={`btn btn-sm ${showHistoryPanel ? 'btn-primary text-white' : 'btn-outline-primary'} py-1 px-2.5 d-inline-flex align-items-center gap-1 shadow-sm`}
onClick={() => setShowHistoryPanel(!showHistoryPanel)}
title="Toggle Patient Old Records & Past Dispensed Medicines (Right-Side)"
style={{ fontSize: "0.75rem" }}
>
<i className="bi bi-clock-history"></i>
<span>Old Records</span>
{patientHistory && patientHistory.total_sales > 0 && (
<span className={`badge ${showHistoryPanel ? 'bg-white text-primary' : 'bg-primary text-white'} ms-1`} style={{ fontSize: "0.65rem" }}>
{patientHistory.total_sales}
</span>
)}
</button>
{(selectedPatient.has_active_opd || selectedPatient.has_active_ipd) && (
<button className="btn btn-sm btn-outline-success py-1 px-2.5" onClick={loadDoctorPrescription}>
<i className="bi bi-file-earmark-medical me-1"></i> 1-Click Load Doctor Rx
@ -2080,6 +2294,7 @@ function PosCounter({ store, notify }) {
)}
</div>
<div className="row g-2">
<div className="col-md-7 position-relative">
<div className="input-group input-group-sm">
@ -2774,8 +2989,399 @@ function PosCounter({ store, notify }) {
</div>
</div>
{/* Right-Side Patient Pharmacy History & Records Panel */}
{selectedPatient && showHistoryPanel && (
<div className="col-12 col-lg-4 col-xl-4">
<div className="patient-history-sidebar position-sticky shadow-sm" style={{ top: '15px', maxHeight: 'calc(100vh - 100px)', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div className="patient-history-header">
<div className="d-flex justify-content-between align-items-center mb-1.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-clock-history text-info fs-5"></i>
<div>
<h6 className="mb-0 fw-bold text-white" style={{ fontSize: '0.9rem' }}>Patient Pharmacy History</h6>
<small className="text-secondary" style={{ fontSize: '0.72rem' }}>
UHID: <span className="text-warning fw-bold">{selectedPatient.uhid}</span>
</small>
</div>
</div>
<div className="d-flex align-items-center gap-1">
<button
type="button"
className="btn btn-xs btn-outline-light py-0.5 px-2"
onClick={() => fetchPatientHistory(selectedPatient.patient_id, selectedPatient.uhid, selectedPatient.mobile)}
disabled={isLoadingHistory}
title="Refresh Patient History"
style={{ fontSize: '0.7rem' }}
>
<i className={`bi bi-arrow-clockwise ${isLoadingHistory ? 'spin' : ''}`}></i>
</button>
<button
type="button"
className="btn btn-xs btn-outline-light py-0.5 px-2"
onClick={() => setShowHistoryPanel(false)}
title="Hide sidebar (Click 'Old Records' in patient bar to re-open)"
style={{ fontSize: '0.7rem' }}
>
<i className="bi bi-chevron-bar-right"></i>
</button>
</div>
</div>
{/* Patient Summary Sub-bar */}
<div className="d-flex flex-wrap align-items-center justify-content-between gap-1 pt-1.5 border-top border-secondary" style={{ fontSize: '0.72rem' }}>
<span className="text-light">
<strong>{selectedPatient.full_name}</strong> • {selectedPatient.gender}, {selectedPatient.age}
</span>
{patientHistory && (
<span className="badge bg-primary-subtle text-primary border border-primary px-1.5 py-0.5">
{patientHistory.total_sales || 0} Bills • ₹{patientHistory.total_spend || 0}
</span>
)}
</div>
</div>
{/* Navigation Tabs */}
<div className="patient-history-tabs">
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'last_given' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('last_given')}
>
<i className="bi bi-capsule text-success"></i>
<span>Last Given</span>
{patientHistory?.last_dispensed_items?.length > 0 && (
<span className="badge bg-success text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.last_dispensed_items.length}
</span>
)}
</button>
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'all_bills' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('all_bills')}
>
<i className="bi bi-receipt text-primary"></i>
<span>Past Bills</span>
{patientHistory?.invoices?.length > 0 && (
<span className="badge bg-primary text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.invoices.length}
</span>
)}
</button>
{patientHistory?.doctor_prescriptions?.length > 0 && (
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'prescriptions' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('prescriptions')}
>
<i className="bi bi-stethoscope text-danger"></i>
<span>Doctor Rx</span>
<span className="badge bg-danger text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.doctor_prescriptions.length}
</span>
</button>
)}
{patientHistory?.unique_medicines?.length > 0 && (
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'summary' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('summary')}
>
<i className="bi bi-journal-medical text-warning"></i>
<span>All Meds</span>
<span className="badge bg-secondary text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.unique_medicines.length}
</span>
</button>
)}
</div>
{/* Body */}
<div className="patient-history-body">
{isLoadingHistory ? (
<div className="text-center py-5 text-muted">
<div className="spinner-border spinner-border-sm text-primary me-2"></div>
Loading patient records...
</div>
) : !patientHistory || (patientHistory.total_sales === 0 && (!patientHistory.doctor_prescriptions || patientHistory.doctor_prescriptions.length === 0)) ? (
<div className="text-center py-4 px-2 text-muted">
<i className="bi bi-folder-x fs-1 text-secondary opacity-50 d-block mb-2"></i>
<h6 className="fw-semibold text-dark">No Previous Pharmacy Records</h6>
<small>This patient has no recorded medicine dispenses or bills in the pharmacy database yet.</small>
</div>
) : (
<>
{/* TAB 1: LAST GIVEN MEDICINES */}
{historyActiveTab === 'last_given' && (
<div>
{patientHistory.last_sale ? (
<>
<div className="p-2.5 mb-2 rounded-3 border" style={{ background: '#f0fdf4', borderColor: '#bbf7d0' }}>
<div className="d-flex justify-content-between align-items-center mb-1">
<span className="fw-bold text-success" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-check-circle-fill me-1"></i> Last Dispensed Visit
</span>
<span className="badge bg-dark text-white" style={{ fontSize: '0.68rem' }}>
{patientHistory.last_sale.invoice_no}
</span>
</div>
<div className="text-muted" style={{ fontSize: '0.72rem' }}>
<div><i className="bi bi-calendar-event me-1"></i> {patientHistory.last_sale.sale_date ? patientHistory.last_sale.sale_date.substring(0, 16) : 'N/A'}</div>
{patientHistory.last_sale.doctor_name && (
<div><i className="bi bi-person-badge me-1"></i> Prescribed by: <strong>{patientHistory.last_sale.doctor_name}</strong></div>
)}
<div className="d-flex justify-content-between align-items-center mt-1.5 pt-1 border-top border-success-subtle">
<span>Paid: <strong>₹{patientHistory.last_sale.net_amount}</strong> ({patientHistory.last_sale.payment_mode})</span>
<button
type="button"
className="btn btn-xs btn-outline-success py-0 px-2 fw-bold"
onClick={() => viewPastBill(patientHistory.last_sale.sale_id)}
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-printer me-1"></i> View Bill
</button>
</div>
</div>
</div>
<div className="fw-bold text-dark mb-1.5 d-flex justify-content-between align-items-center" style={{ fontSize: '0.75rem' }}>
<span>Medicines Given ({patientHistory.last_dispensed_items.length}):</span>
<small className="text-muted">Click <strong>+</strong> to re-dispense</small>
</div>
{patientHistory.last_dispensed_items.map((it, mIdx) => (
<div key={`last-med-${mIdx}`} className="history-med-card last-given">
<div className="d-flex justify-content-between align-items-start mb-1">
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>
{it.item_name}
</div>
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>
{it.generic_name}
</small>
</div>
<div className="text-end">
<span className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>₹{it.total_amount}</span>
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>₹{it.unit_mrp}/{it.sell_unit}</small>
</div>
</div>
<div className="d-flex flex-wrap align-items-center gap-1 mb-2" style={{ fontSize: '0.68rem' }}>
<span className="badge bg-light text-dark border">Batch: {it.batch_no}</span>
<span className="badge bg-light text-secondary border">Exp: {it.expiry_date}</span>
<span className="badge bg-primary text-white">Qty: {it.qty} {it.sell_unit}</span>
{it.remaining_units > 0 ? (
<span className="badge bg-success-subtle text-success border border-success">
Returnable: {it.remaining_strips > 0 ? `${it.remaining_strips} Str ` : ''}{it.remaining_loose > 0 ? `${it.remaining_loose} Tab` : ''}
</span>
) : (
<span className="badge bg-secondary-subtle text-muted border">Fully Returned</span>
)}
</div>
{/* Actions */}
<div className="d-flex gap-1.5 pt-1.5 border-top">
<button
type="button"
className="btn btn-xs btn-success py-1 px-2 fw-semibold d-inline-flex align-items-center gap-1 shadow-xs flex-fill justify-content-center"
onClick={() => reDispenseMedicine(it, it.qty)}
style={{ fontSize: '0.72rem', background: '#059669', borderColor: '#047857' }}
title="Add this medicine to active dispensing cart with current FEFO batch"
>
<i className="bi bi-plus-circle-fill"></i>
<span>Dispense Again</span>
</button>
{it.remaining_units > 0 && (
<button
type="button"
className="btn btn-xs btn-outline-danger py-1 px-2 fw-semibold d-inline-flex align-items-center gap-1 shadow-xs"
onClick={() => initiateReturnFromHistory(patientHistory.last_sale, it)}
style={{ fontSize: '0.72rem' }}
title="Return or exchange this dispensed item into current bill"
>
<i className="bi bi-arrow-return-left"></i>
<span>Return</span>
</button>
)}
</div>
</div>
))}
</>
) : (
<div className="text-center py-4 text-muted">
<i className="bi bi-basket text-secondary fs-3 d-block mb-1"></i>
No pharmacy sales recorded yet for this patient.
</div>
)}
</div>
)}
{/* TAB 2: ALL PAST BILLS */}
{historyActiveTab === 'all_bills' && (
<div>
{patientHistory.invoices?.map((inv, invIdx) => {
const s = inv.sale;
const isExp = expandedBillId === s.sale_id;
return (
<div key={`inv-${s.sale_id}`} className="card mb-2 border shadow-xs" style={{ borderRadius: '8px' }}>
<div
className="card-header py-2 px-2.5 bg-white d-flex justify-content-between align-items-center"
style={{ cursor: 'pointer', borderBottom: isExp ? '1px solid #e2e8f0' : 'none' }}
onClick={() => setExpandedBillId(isExp ? null : s.sale_id)}
>
<div>
<div className="fw-bold text-dark d-flex align-items-center gap-1.5" style={{ fontSize: '0.78rem' }}>
<i className={`bi bi-chevron-${isExp ? 'down' : 'right'} text-primary`} style={{ fontSize: '0.7rem' }}></i>
<span>{s.invoice_no}</span>
<span className="badge bg-light text-secondary border" style={{ fontSize: '0.65rem' }}>{inv.items.length} items</span>
</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>
{s.sale_date ? s.sale_date.substring(0, 10) : ''} • Dr. {s.doctor_name || 'Consultant'}
</small>
</div>
<div className="text-end">
<span className="fw-bold text-success d-block" style={{ fontSize: '0.82rem' }}>₹{s.net_amount}</span>
<span className="badge bg-secondary-subtle text-secondary" style={{ fontSize: '0.62rem' }}>{s.payment_mode}</span>
</div>
</div>
{isExp && (
<div className="card-body p-2 bg-light">
<div className="d-flex justify-content-between align-items-center mb-1.5">
<span className="text-muted fw-semibold" style={{ fontSize: '0.7rem' }}>Items Dispensed:</span>
<div className="d-flex gap-1">
<button
type="button"
className="btn btn-xs btn-outline-dark py-0 px-1.5"
onClick={(e) => { e.stopPropagation(); viewPastBill(s.sale_id); }}
style={{ fontSize: '0.65rem' }}
>
<i className="bi bi-printer me-1"></i> Reprint
</button>
<button
type="button"
className="btn btn-xs btn-outline-danger py-0 px-1.5"
onClick={(e) => { e.stopPropagation(); selectReturnInvoice(inv); openReturnModal(); }}
style={{ fontSize: '0.65rem' }}
>
<i className="bi bi-arrow-return-left me-1"></i> Return All
</button>
</div>
</div>
{inv.items.map((it, itIdx) => (
<div key={`inv-item-${itIdx}`} className="p-1.5 mb-1 bg-white rounded border d-flex justify-content-between align-items-center" style={{ fontSize: '0.72rem' }}>
<div>
<div className="fw-bold text-dark">{it.item_name}</div>
<div className="text-muted" style={{ fontSize: '0.65rem' }}>
Batch: {it.batch_no} • {it.qty} {it.sell_unit} @ ₹{it.unit_mrp}
</div>
</div>
<div className="d-flex align-items-center gap-1">
<button
type="button"
className="btn btn-xs btn-outline-success py-0 px-1"
onClick={() => reDispenseMedicine(it, it.qty)}
title="Dispense again"
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-plus"></i>
</button>
{it.remaining_units > 0 && (
<button
type="button"
className="btn btn-xs btn-outline-danger py-0 px-1"
onClick={() => initiateReturnFromHistory(s, it)}
title="Return this item"
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-arrow-return-left"></i>
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
)}
{/* TAB 3: DOCTOR PRESCRIPTIONS */}
{historyActiveTab === 'prescriptions' && (
<div>
<div className="text-muted mb-2 fw-semibold" style={{ fontSize: '0.72rem' }}>
<i className="bi bi-stethoscope text-primary me-1"></i> Doctor Prescriptions from OPD Visits:
</div>
{patientHistory.doctor_prescriptions?.map((dp, dpIdx) => (
<div key={`doc-rx-${dpIdx}`} className="history-med-card mb-2">
<div className="d-flex justify-content-between align-items-start mb-1">
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>{dp.med_name}</div>
<span className="badge bg-secondary-subtle text-secondary border" style={{ fontSize: '0.65rem' }}>{dp.med_type || 'Medicine'}</span>
</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>{dp.date_opd_visit}</small>
</div>
<div className="text-muted mb-1.5" style={{ fontSize: '0.7rem' }}>
{dp.doc_name && <div>Doc: <strong>Dr. {dp.doc_name}</strong></div>}
<div>Dose: {dp.dosage_freq_str || dp.dosage || '1 Tab'} • Duration: {dp.no_of_days || '5 Days'}</div>
{dp.remark && <div>Note: {dp.remark}</div>}
</div>
<button
type="button"
className="btn btn-xs btn-primary w-100 py-1 fw-semibold d-flex align-items-center justify-content-center gap-1 shadow-xs"
onClick={() => reDispenseMedicine({ item_name: dp.med_name, qty: 10 })}
style={{ fontSize: '0.72rem' }}
>
<i className="bi bi-plus-circle"></i> Dispense Prescribed Medicine
</button>
</div>
))}
</div>
)}
{/* TAB 4: ALL MEDS SUMMARY */}
{historyActiveTab === 'summary' && (
<div>
<div className="text-muted mb-2 fw-semibold" style={{ fontSize: '0.72rem' }}>
Distinct Medicines Dispensed to Patient:
</div>
{patientHistory.unique_medicines?.map((um, uIdx) => (
<div key={`uniq-med-${uIdx}`} className="history-med-card mb-2">
<div className="d-flex justify-content-between align-items-start mb-1">
<div className="fw-bold text-dark" style={{ fontSize: '0.8rem' }}>{um.item_name}</div>
<span className="badge bg-primary-subtle text-primary border" style={{ fontSize: '0.65rem' }}>
{um.times_dispensed}x dispensed
</span>
</div>
<small className="text-muted d-block mb-1.5" style={{ fontSize: '0.68rem' }}>
Last on {um.last_dispensed ? um.last_dispensed.substring(0, 10) : ''} (Batch: {um.last_batch_no})
</small>
<button
type="button"
className="btn btn-xs btn-outline-success w-100 py-0.5 fw-semibold d-flex align-items-center justify-content-center gap-1"
onClick={() => reDispenseMedicine(um, um.last_qty || 1)}
style={{ fontSize: '0.7rem' }}
>
<i className="bi bi-plus-circle"></i> Dispense Again
</button>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
</div>
)}
</div>
{/* Slide-in Checkout Desk Drawer & Backdrop */}
<div
className={`checkout-drawer-overlay ${isCheckoutOpen ? 'open' : ''}`}