feat(pharmacy): add expired medicine disposal, quick return and rich product search

- Add direct expiry disposal (write-off) and quick return from Nearest Expiry report
- Add Disposal Register view and PDF audit certificate
- Add supplier info and rich selectable autocomplete in purchase return search
- Register disposal & quick return routes in Routes.php
This commit is contained in:
DevSoftTech Server 2026-10-25 23:27:06 +05:30
parent 51c06304d0
commit 083a82dcb6
8 changed files with 1283 additions and 74 deletions

View File

@ -222,6 +222,14 @@ $routes->get('Medical_backpanel/Stock_result_excel/(:segment)/(:segment)/(:segme
$routes->get('Medical_backpanel/Stock_result_excel_3/(:segment)/(:segment)/(:segment)/(:segment)', 'Medical::stock_result_excel_3/$1/$2/$3/$4');
$routes->get('Medical/expire_stock', 'Medical::expire_stock');
$routes->get('Medical/expire_stock_pdf', 'Medical::expire_stock_pdf');
$routes->post('Medical/dispose_expired_stock', 'Medical::dispose_expired_stock');
$routes->post('Medical/quick_create_purchase_return', 'Medical::quick_create_purchase_return');
$routes->match(['GET', 'POST'], 'Medical/disposal_register', 'Medical::disposal_register');
$routes->get('Medical/disposal_register_pdf', 'Medical::disposal_register_pdf');
$routes->post('Medical_backpanel/dispose_expired_stock', 'Medical::dispose_expired_stock');
$routes->post('Medical_backpanel/quick_create_purchase_return', 'Medical::quick_create_purchase_return');
$routes->match(['GET', 'POST'], 'Medical_backpanel/disposal_register', 'Medical::disposal_register');
$routes->get('Medical_backpanel/disposal_register_pdf', 'Medical::disposal_register_pdf');
$routes->get('Medical/stocktransfer', 'Medical::stocktransfer');
$routes->get('Medical/merge_product', 'Medical::merge_product');
$routes->post('Medical/product_info/(:num)', 'Medical::product_info/$1');

View File

@ -562,19 +562,37 @@ class Medical extends BaseController
$params[] = '%' . $term . '%';
}
$hasPi = $this->db->tableExists('purchase_invoice');
$hasSupp = $this->db->tableExists('med_supplier');
$piJoin = ($hasPi && in_array('purchase_id', $tFields, true)) ? ' LEFT JOIN purchase_invoice pi ON t.purchase_id = pi.id' : '';
$suppJoin = ($hasPi && $hasSupp) ? ' LEFT JOIN med_supplier s ON pi.sid = s.sid' : '';
$suppNameCol = ($hasPi && $hasSupp) ? "IFNULL(s.name_supplier, IFNULL(pi.supp_name, '-'))" : "'-'";
$suppShortCol = ($hasPi && $hasSupp) ? "IFNULL(s.short_name, '')" : "''";
$suppIdCol = $hasPi ? "IFNULL(pi.sid, 0)" : "0";
$purRateCol = in_array('purchase_unit_rate', $tFields, true) ? 't.purchase_unit_rate' : '0';
$purIdCol = in_array('purchase_id', $tFields, true) ? 't.purchase_id' : '0';
$sql = "SELECT t.id, p.id AS item_code, p.item_name AS item_name,
" . (in_array('formulation', $pFields, true) ? 'p.formulation' : "''") . " AS formulation,
" . (in_array('batch_no', $tFields, true) ? 't.batch_no' : "''") . " AS batch_no,
" . (in_array('expiry_date', $tFields, true) ? 't.expiry_date' : 'NULL') . " AS expiry_date,
" . (in_array('expiry_date', $tFields, true) ? "DATE_FORMAT(t.expiry_date, '%Y-%m-%d')" : "''") . " AS expiry_date_str,
" . (in_array('expiry_date', $tFields, true) ? "DATE_FORMAT(t.expiry_date, '%d/%m/%Y')" : "''") . " AS expiry_date_display,
" . (in_array('mrp', $tFields, true) ? 't.mrp' : '0') . " AS mrp,
" . (in_array('selling_unit_rate', $tFields, true) ? 't.selling_unit_rate' : '0') . " AS selling_unit_rate,
" . $purRateCol . " AS purchase_unit_rate,
" . (in_array('packing', $tFields, true) ? 't.packing' : '1') . " AS packing,
" . $stockQtySql . " AS c_qty,
" . (in_array('expiry_date', $tFields, true) ? 'DATEDIFF(LAST_DAY(t.expiry_date), CURDATE())' : '9999') . " AS expiry_month_pending,
" . (in_array('stock_date', $tFields, true) ? "IF(t.stock_date>'2025-09-21',1,0)" : '0') . " AS new_stock
" . (in_array('stock_date', $tFields, true) ? "IF(t.stock_date>'2025-09-21',1,0)" : '0') . " AS new_stock,
" . $purIdCol . " AS purchase_id,
" . $suppNameCol . " AS supplier_name,
" . $suppShortCol . " AS supplier_short_name,
" . $suppIdCol . " AS supplier_id
FROM med_product_master p
JOIN purchase_invoice_item t ON p.id=t.item_code
" . $piJoin . "
" . $suppJoin . "
WHERE " . $whereSearch . "
AND " . $stockQtySql . " > 0
ORDER BY item_name, t.id DESC
@ -585,18 +603,36 @@ class Medical extends BaseController
$itemNameCol = in_array('item_name', $tFields, true) ? 't.item_name' : (in_array('Item_name', $tFields, true) ? 't.Item_name' : "''");
$itemCodeCol = in_array('item_code', $tFields, true) ? 't.item_code' : '0';
$hasPi = $this->db->tableExists('purchase_invoice');
$hasSupp = $this->db->tableExists('med_supplier');
$piJoin = ($hasPi && in_array('purchase_id', $tFields, true)) ? ' LEFT JOIN purchase_invoice pi ON t.purchase_id = pi.id' : '';
$suppJoin = ($hasPi && $hasSupp) ? ' LEFT JOIN med_supplier s ON pi.sid = s.sid' : '';
$suppNameCol = ($hasPi && $hasSupp) ? "IFNULL(s.name_supplier, IFNULL(pi.supp_name, '-'))" : "'-'";
$suppShortCol = ($hasPi && $hasSupp) ? "IFNULL(s.short_name, '')" : "''";
$suppIdCol = $hasPi ? "IFNULL(pi.sid, 0)" : "0";
$purRateCol = in_array('purchase_unit_rate', $tFields, true) ? 't.purchase_unit_rate' : '0';
$purIdCol = in_array('purchase_id', $tFields, true) ? 't.purchase_id' : '0';
$sql = "SELECT t.id, " . $itemCodeCol . " AS item_code, " . $itemNameCol . " AS item_name,
" . (in_array('formulation', $tFields, true) ? 't.formulation' : "''") . " AS formulation,
" . (in_array('batch_no', $tFields, true) ? 't.batch_no' : "''") . " AS batch_no,
" . (in_array('expiry_date', $tFields, true) ? 't.expiry_date' : 'NULL') . " AS expiry_date,
" . (in_array('expiry_date', $tFields, true) ? "DATE_FORMAT(t.expiry_date, '%Y-%m-%d')" : "''") . " AS expiry_date_str,
" . (in_array('expiry_date', $tFields, true) ? "DATE_FORMAT(t.expiry_date, '%d/%m/%Y')" : "''") . " AS expiry_date_display,
" . (in_array('mrp', $tFields, true) ? 't.mrp' : '0') . " AS mrp,
" . (in_array('selling_unit_rate', $tFields, true) ? 't.selling_unit_rate' : '0') . " AS selling_unit_rate,
" . $purRateCol . " AS purchase_unit_rate,
" . (in_array('packing', $tFields, true) ? 't.packing' : '1') . " AS packing,
" . $stockQtySql . " AS c_qty,
" . (in_array('expiry_date', $tFields, true) ? 'DATEDIFF(LAST_DAY(t.expiry_date), CURDATE())' : '9999') . " AS expiry_month_pending,
" . (in_array('stock_date', $tFields, true) ? "IF(t.stock_date>'2025-09-21',1,0)" : '0') . " AS new_stock
" . (in_array('stock_date', $tFields, true) ? "IF(t.stock_date>'2025-09-21',1,0)" : '0') . " AS new_stock,
" . $purIdCol . " AS purchase_id,
" . $suppNameCol . " AS supplier_name,
" . $suppShortCol . " AS supplier_short_name,
" . $suppIdCol . " AS supplier_id
FROM purchase_invoice_item t
" . $piJoin . "
" . $suppJoin . "
WHERE (" . $itemNameCol . " LIKE ?)
AND " . $stockQtySql . " > 0
ORDER BY item_name, t.id DESC
@ -609,16 +645,26 @@ class Medical extends BaseController
foreach ($rows as $row) {
$pendingDays = (float) ($row['expiry_month_pending'] ?? 9999);
$noExpMonth = round($pendingDays / 30, 2);
$isExpired = ($pendingDays <= 0);
$msgDesc = '';
if ($noExpMonth <= 4 && $noExpMonth >= 2) {
if ($isExpired) {
$monthsAgo = abs($noExpMonth);
$msgDesc = ' <span style="color: red; font-weight: 600;">Expired' . ($monthsAgo > 0 ? ' : ' . $monthsAgo . ' Month' : '') . ' </span>';
} elseif ($noExpMonth <= 1.0) {
$msgDesc = ' <span style="color: Orange; font-weight: 500;">Expire Within : ' . $noExpMonth . ' Month </span>';
} elseif ($noExpMonth <= 4.0) {
$msgDesc = ' <span style="color: blue;">Expire Within : ' . $noExpMonth . ' Month </span>';
} elseif ($noExpMonth == 1.0) {
$msgDesc = ' <span style="color: Orange;">Expired : ' . $noExpMonth . ' Month </span>';
} elseif ($noExpMonth < 1) {
$msgDesc = ' <span style="color: red;">Expired : ' . $noExpMonth . ' Month </span>';
}
$purUnitRate = (float) ($row['purchase_unit_rate'] ?? 0);
if ($purUnitRate <= 0) {
$packing = (float) ($row['packing'] ?? 1);
$purUnitRate = $packing > 0 ? round(((float) ($row['mrp'] ?? 0)) / $packing, 4) : (float) ($row['selling_unit_rate'] ?? 0);
}
$expDisplay = (string) ($row['expiry_date_display'] ?? ($row['expiry_date_str'] ?? ''));
$result[] = [
'label' => trim((string) ($row['item_name'] ?? '') . ' ' . (string) ($row['formulation'] ?? ''))
. ' |B:' . (string) ($row['batch_no'] ?? '')
@ -632,16 +678,23 @@ class Medical extends BaseController
'l_ss_no' => (string) ($row['id'] ?? ''),
'l_Batch' => (string) ($row['batch_no'] ?? ''),
'l_Expiry' => (string) ($row['expiry_date_str'] ?? ''),
'l_expiry_display' => $expDisplay,
'l_mrp' => (string) ($row['mrp'] ?? ''),
'l_unit_rate' => (string) (
((float) ($row['packing'] ?? 0) > 0)
? round(((float) ($row['mrp'] ?? 0)) / ((float) ($row['packing'] ?? 1)), 4)
: (float) ($row['selling_unit_rate'] ?? 0)
),
'l_pur_unit_rate' => (string) ($purUnitRate > 0 ? round($purUnitRate, 4) : 0),
'l_purchase_id' => (string) ($row['purchase_id'] ?? 0),
'l_c_qty' => (string) ($row['c_qty'] ?? ''),
'l_packing' => (string) ($row['packing'] ?? ''),
'l_new_stock' => (string) ($row['new_stock'] ?? 0),
'item_code' => (string) ($row['item_code'] ?? ''),
'supplier_name' => (string) ($row['supplier_name'] ?? '-'),
'supplier_short_name' => (string) ($row['supplier_short_name'] ?? ''),
'supplier_id' => (int) ($row['supplier_id'] ?? 0),
'is_expired' => $isExpired ? 1 : 0,
];
}
@ -12827,7 +12880,9 @@ class Medical extends BaseController
$sql = "SELECT p.id AS pur_id, p.invoice_no, p.date_of_invoice,
DATE_FORMAT(p.date_of_invoice,'%d-%m-%Y') AS str_date_of_invoice,
DATE_FORMAT(i.expiry_date,'%m-%Y') AS exp_date,
DATE_FORMAT(i.expiry_date,'%d-%m-%Y') AS exp_date_full,
p.sid, s.name_supplier, s.short_name,
i.id AS purchase_item_id,
i.*, (i.total_unit-i.total_sale_unit-i.total_lost_unit-i.total_return_unit) AS cur_qty
FROM purchase_invoice p
JOIN med_supplier s ON p.sid=s.sid
@ -12835,7 +12890,7 @@ class Medical extends BaseController
WHERE i.remove_item=0
AND i.item_return=0
AND (i.total_unit-i.total_sale_unit-i.total_lost_unit-i.total_return_unit)>0
AND p.date_of_invoice>=DATE_ADD(CURDATE(), INTERVAL -1 YEAR)
AND p.date_of_invoice>=DATE_ADD(CURDATE(), INTERVAL -2 YEAR)
AND i.expiry_date<DATE_ADD(CURDATE(), INTERVAL 3 MONTH)
ORDER BY p.id DESC";
@ -12883,6 +12938,362 @@ class Medical extends BaseController
->setBody($mpdf->Output('Expiry_Medicine_Report_' . date('Ymd_His') . '.pdf', 'S'));
}
private function ensureDisposalTableExists(): void
{
$this->db->query("CREATE TABLE IF NOT EXISTS `med_stock_disposal` (
`id` int NOT NULL AUTO_INCREMENT,
`disposal_no` varchar(50) DEFAULT NULL,
`purchase_item_id` int NOT NULL DEFAULT '0',
`purchase_id` int NOT NULL DEFAULT '0',
`item_code` int DEFAULT NULL,
`item_name` varchar(100) DEFAULT NULL,
`batch_no` varchar(50) DEFAULT NULL,
`expiry_date` date DEFAULT NULL,
`sid` int DEFAULT '0',
`supplier_name` varchar(100) DEFAULT NULL,
`qty` decimal(10,2) NOT NULL DEFAULT '0.00',
`mrp` decimal(10,2) DEFAULT '0.00',
`purchase_unit_rate` decimal(10,2) DEFAULT '0.00',
`total_cost_amount` decimal(10,2) DEFAULT '0.00',
`disposal_reason` varchar(100) DEFAULT 'Expired / Outdated',
`disposal_method` varchar(100) DEFAULT 'Bio-Medical Waste Handover',
`remarks` text DEFAULT NULL,
`disposed_by_user_id` int DEFAULT '0',
`disposed_by_name` varchar(100) DEFAULT NULL,
`disposal_date` date NOT NULL,
`insert_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_disp_item_id` (`purchase_item_id`),
KEY `idx_disp_item_code` (`item_code`),
KEY `idx_disp_date` (`disposal_date`),
KEY `idx_disp_no` (`disposal_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
private function generateDisposalNo(): string
{
$prefix = 'EXP' . date('ym');
$maxRow = $this->db->table('med_stock_disposal')
->like('disposal_no', $prefix, 'after')
->orderBy('id', 'DESC')
->get(1)
->getRow();
$seq = 1;
if ($maxRow && ! empty($maxRow->disposal_no)) {
$numPart = (int) substr($maxRow->disposal_no, strlen($prefix));
$seq = $numPart + 1;
}
return $prefix . str_pad((string) $seq, 3, '0', STR_PAD_LEFT);
}
public function dispose_expired_stock()
{
if ($deny = $this->ensurePharmacyAccess()) {
return $deny;
}
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Invalid request']);
}
$this->ensureDisposalTableExists();
$itemId = (int) ($this->request->getPost('item_id') ?? 0);
$qty = (float) ($this->request->getPost('qty') ?? 0);
$reason = trim((string) ($this->request->getPost('reason') ?? 'Expired / Outdated'));
$method = trim((string) ($this->request->getPost('method') ?? 'Bio-Medical Waste Handover'));
$remarks = trim((string) ($this->request->getPost('remarks') ?? ''));
if ($itemId <= 0 || $qty <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Invalid item or quantity']);
}
$item = $this->db->table('purchase_invoice_item i')
->select("i.*, p.sid, IFNULL(s.name_supplier, IFNULL(p.supp_name, '-')) AS name_supplier, (i.total_unit - i.total_sale_unit - i.total_lost_unit - i.total_return_unit) AS cur_qty", false)
->join('purchase_invoice p', 'i.purchase_id = p.id', 'left')
->join('med_supplier s', 'p.sid = s.sid', 'left')
->where('i.id', $itemId)
->get()
->getRow();
if (! $item) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Stock batch not found']);
}
$curQty = (float) ($item->cur_qty ?? 0);
if ($curQty <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Current stock is 0. Already disposed or returned.']);
}
if ($qty > $curQty) {
return $this->response->setJSON(['status' => 0, 'msg' => "Disposal quantity ({$qty}) cannot exceed available stock ({$curQty})"]);
}
$user = service('auth')->user();
$userId = $user ? (int) $user->id : (int) (session('user_id') ?? 0);
$userName = $user ? (string) ($user->username ?? $user->email ?? 'Pharmacist') : (string) (session('username') ?? 'Pharmacist');
$purRate = (float) ($item->purchase_unit_rate ?? 0);
if ($purRate <= 0) {
$packing = (float) ($item->packing ?? 1);
$purRate = $packing > 0 ? round(((float) ($item->mrp ?? 0)) / $packing, 2) : (float) ($item->selling_unit_rate ?? 0);
}
$totalCost = round($qty * $purRate, 2);
$disposalNo = $this->generateDisposalNo();
$disposalData = [
'disposal_no' => $disposalNo,
'purchase_item_id' => $itemId,
'purchase_id' => (int) ($item->purchase_id ?? 0),
'item_code' => (int) ($item->item_code ?? 0),
'item_name' => (string) ($item->Item_name ?? ($item->item_name ?? '')),
'batch_no' => (string) ($item->batch_no ?? ''),
'expiry_date' => $item->expiry_date ?? null,
'sid' => (int) ($item->sid ?? 0),
'supplier_name' => (string) ($item->name_supplier ?? '-'),
'qty' => $qty,
'mrp' => (float) ($item->mrp ?? 0),
'purchase_unit_rate' => $purRate,
'total_cost_amount' => $totalCost,
'disposal_reason' => $reason !== '' ? $reason : 'Expired / Outdated',
'disposal_method' => $method !== '' ? $method : 'Bio-Medical Waste Handover',
'remarks' => $remarks !== '' ? $remarks : null,
'disposed_by_user_id' => $userId,
'disposed_by_name' => $userName,
'disposal_date' => date('Y-m-d'),
];
$this->db->table('med_stock_disposal')->insert($disposalData);
$dispId = (int) $this->db->insertID();
if ($dispId <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Failed to record disposal']);
}
// Increment total_lost_unit in purchase_invoice_item
$this->db->table('purchase_invoice_item')
->where('id', $itemId)
->set('total_lost_unit', "total_lost_unit + {$qty}", false)
->update();
$remainingQty = $curQty - $qty;
return $this->response->setJSON([
'status' => 1,
'msg' => "Successfully Disposed {$qty} units (Voucher: {$disposalNo}). Stock removed from inventory.",
'disposal_no' => $disposalNo,
'disposal_id' => $dispId,
'remaining_qty' => $remainingQty,
]);
}
public function quick_create_purchase_return()
{
if ($deny = $this->ensurePharmacyAccess()) {
return $deny;
}
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Invalid request']);
}
$itemId = (int) ($this->request->getPost('item_id') ?? 0);
$qty = (float) ($this->request->getPost('qty') ?? 0);
if ($itemId <= 0 || $qty <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Invalid item or quantity']);
}
$item = $this->db->table('purchase_invoice_item i')
->select("i.*, p.sid, IFNULL(s.name_supplier, IFNULL(p.supp_name, '-')) AS name_supplier, (i.total_unit - i.total_sale_unit - i.total_lost_unit - i.total_return_unit) AS cur_qty", false)
->join('purchase_invoice p', 'i.purchase_id = p.id', 'left')
->join('med_supplier s', 'p.sid = s.sid', 'left')
->where('i.id', $itemId)
->get()
->getRow();
if (! $item) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Stock batch not found']);
}
$curQty = (float) ($item->cur_qty ?? 0);
if ($curQty <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'Current stock is 0.']);
}
if ($qty > $curQty) {
return $this->response->setJSON(['status' => 0, 'msg' => "Return quantity ({$qty}) cannot exceed available stock ({$curQty})"]);
}
$sid = (int) ($item->sid ?? 0);
if ($sid <= 0) {
return $this->response->setJSON(['status' => 0, 'msg' => 'No supplier linked with this purchase item. Cannot create supplier return.']);
}
$invNoCol = $this->purchaseReturnInvoiceNoColumn();
// Check if there is an open/draft purchase return invoice today for this supplier
$existingInv = $this->db->table('purchase_return_invoice')
->where('sid', $sid)
->where('date_of_invoice', date('Y-m-d'))
->where('(status = 0 OR status IS NULL)', null, false)
->orderBy('id', 'DESC')
->get(1)
->getRow();
if ($existingInv) {
$invId = (int) $existingInv->id;
$invoiceNo = (string) ($existingInv->{$invNoCol} ?? ('PR#' . $invId));
} else {
// Create a new purchase return invoice
$insertData = [
'sid' => $sid,
'date_of_invoice' => date('Y-m-d'),
'status' => 0,
];
$this->db->table('purchase_return_invoice')->insert($insertData);
$invId = (int) $this->db->insertID();
$invoiceNo = 'PR' . date('ym') . str_pad((string) ($invId % 1000), 3, '0', STR_PAD_LEFT);
$this->db->table('purchase_return_invoice')
->where('id', $invId)
->update([$invNoCol => $invoiceNo]);
}
// Insert into purchase_return_invoice_item
$retItemData = [
'purchase_inv_id' => $invId,
'purchase_item_id' => $itemId,
'item_code' => (int) ($item->item_code ?? 0),
'Item_name' => (string) ($item->Item_name ?? ($item->item_name ?? '')),
'batch_no_r' => (string) ($item->batch_no ?? ''),
'expiry_date_r' => $item->expiry_date ?? null,
'qty' => $qty,
];
$this->db->table('purchase_return_invoice_item')->insert($retItemData);
// Adjust purchase item return unit
$this->adjustPurchaseItemReturnUnit($itemId, $qty);
return $this->response->setJSON([
'status' => 1,
'msg' => "Item successfully added to Return Invoice {$invoiceNo} (Supplier: {$item->name_supplier})",
'inv_id' => $invId,
'invoice_no' => $invoiceNo,
'supplier_name' => (string) ($item->name_supplier ?? '-'),
]);
}
public function disposal_register()
{
if ($deny = $this->ensurePharmacyAccess()) {
return $deny;
}
$this->ensureDisposalTableExists();
$fromDt = $this->request->getGet('from_date') ?: date('Y-m-01');
$toDt = $this->request->getGet('to_date') ?: date('Y-m-d');
$search = trim((string) ($this->request->getGet('search') ?? ''));
$builder = $this->db->table('med_stock_disposal')
->where('disposal_date >=', $fromDt)
->where('disposal_date <=', $toDt);
if ($search !== '') {
$builder->groupStart()
->like('item_name', $search)
->orLike('batch_no', $search)
->orLike('disposal_no', $search)
->orLike('supplier_name', $search)
->groupEnd();
}
$records = $builder->orderBy('id', 'DESC')->get()->getResult();
$totalUnits = 0.0;
$totalLoss = 0.0;
foreach ($records as $r) {
$totalUnits += (float) ($r->qty ?? 0);
$totalLoss += (float) ($r->total_cost_amount ?? 0);
}
return view('medical/disposal_register', [
'records' => $records,
'from_date' => $fromDt,
'to_date' => $toDt,
'search' => $search,
'total_units' => $totalUnits,
'total_loss' => $totalLoss,
]);
}
public function disposal_register_pdf()
{
if ($deny = $this->ensurePharmacyAccess()) {
return $deny;
}
$this->ensureDisposalTableExists();
$fromDt = $this->request->getGet('from_date') ?: date('Y-m-01');
$toDt = $this->request->getGet('to_date') ?: date('Y-m-d');
$search = trim((string) ($this->request->getGet('search') ?? ''));
$builder = $this->db->table('med_stock_disposal')
->where('disposal_date >=', $fromDt)
->where('disposal_date <=', $toDt);
if ($search !== '') {
$builder->groupStart()
->like('item_name', $search)
->orLike('batch_no', $search)
->orLike('disposal_no', $search)
->orLike('supplier_name', $search)
->groupEnd();
}
$records = $builder->orderBy('id', 'DESC')->get()->getResult();
$totalUnits = 0.0;
$totalLoss = 0.0;
foreach ($records as $r) {
$totalUnits += (float) ($r->qty ?? 0);
$totalLoss += (float) ($r->total_cost_amount ?? 0);
}
$html = view('medical/disposal_register_pdf', [
'records' => $records,
'from_date' => $fromDt,
'to_date' => $toDt,
'total_units' => $totalUnits,
'total_loss' => $totalLoss,
'generated_at' => date('d-m-Y H:i'),
]);
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'orientation' => 'L',
'margin_left' => 8,
'margin_right' => 8,
'margin_top' => 8,
'margin_bottom' => 8,
'tempDir' => WRITEPATH . 'cache',
]);
$mpdf->SetTitle('Expired Medicine Disposal Register');
$mpdf->WriteHTML($html);
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setBody($mpdf->Output('Disposal_Register_' . date('Ymd_His') . '.pdf', 'S'));
}
public function stocktransfer()
{
if ($deny = $this->ensurePharmacyAccess()) {

View File

@ -0,0 +1,149 @@
<?php
$records = $records ?? [];
$fromDate = $from_date ?? date('Y-m-01');
$toDate = $to_date ?? date('Y-m-d');
$search = $search ?? '';
$totalUnits = (float) ($total_units ?? 0);
$totalLoss = (float) ($total_loss ?? 0);
?>
<div class="card">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<h5 class="card-title mb-0"><i class="fa fa-archive text-danger me-2"></i>Expired &amp; Damaged Medicine Disposal Register</h5>
<small class="text-muted">Permanent Loss &amp; Destruction Audit Record (NABH / BMW Compliant)</small>
</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-sm btn-outline-danger" target="_blank" href="<?= base_url('Medical/disposal_register_pdf?from_date=' . urlencode($fromDate) . '&to_date=' . urlencode($toDate) . '&search=' . urlencode($search)) ?>">
<i class="fa fa-print me-1"></i> Print Register (PDF)
</a>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="load_form_div('<?= base_url('Medical/expire_stock') ?>','medical-main','Nearest Expiry Stock :Pharmacy');">
<i class="fa fa-arrow-left me-1"></i> Back to Expiry Stock
</button>
</div>
</div>
<div class="card-body pt-3">
<!-- Date & Keyword Filter -->
<form id="disposal_filter_form" onsubmit="event.preventDefault(); applyDisposalFilter();" class="row g-2 align-items-end mb-3">
<div class="col-md-3 col-sm-6">
<label class="form-label small fw-semibold">From Date</label>
<input type="date" class="form-control form-control-sm" id="disp_from_date" value="<?= esc($fromDate) ?>">
</div>
<div class="col-md-3 col-sm-6">
<label class="form-label small fw-semibold">To Date</label>
<input type="date" class="form-control form-control-sm" id="disp_to_date" value="<?= esc($toDate) ?>">
</div>
<div class="col-md-4 col-sm-8">
<label class="form-label small fw-semibold">Search Medicine / Batch / Voucher</label>
<input type="text" class="form-control form-control-sm" id="disp_search" placeholder="Type name, batch, voucher, supplier..." value="<?= esc($search) ?>">
</div>
<div class="col-md-2 col-sm-4 d-flex gap-1">
<button type="submit" class="btn btn-primary btn-sm w-100"><i class="fa fa-filter me-1"></i> Filter</button>
<button type="button" class="btn btn-light btn-sm border" onclick="resetDisposalFilter()" title="Reset"><i class="fa fa-refresh"></i></button>
</div>
</form>
<!-- KPI Summary Cards -->
<div class="row g-2 mb-3">
<div class="col-md-4 col-12">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">Total Disposed Items</div>
<div class="h5 mb-0 text-dark fw-bold"><?= count($records) ?> <small class="text-muted fs-6 font-monospace">batches</small></div>
</div>
</div>
<div class="col-md-4 col-12">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">Total Quantity Disposed / Lost</div>
<div class="h5 mb-0 text-danger fw-bold"><?= number_format($totalUnits, 2) ?> <small class="text-muted fs-6">units</small></div>
</div>
</div>
<div class="col-md-4 col-12">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">Total Purchase Cost (Financial Loss)</div>
<div class="h5 mb-0 text-primary fw-bold">₹ <?= number_format($totalLoss, 2) ?></div>
</div>
</div>
</div>
<!-- Records Table -->
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover table-sm align-middle" style="font-size: 0.85rem;">
<thead class="table-dark">
<tr>
<th>#</th>
<th>Voucher No.</th>
<th>Date</th>
<th>Medicine Name</th>
<th>Batch</th>
<th>Exp. Date</th>
<th>Supplier</th>
<th class="text-end">Disposed Qty</th>
<th class="text-end">P. Rate</th>
<th class="text-end">Loss Value</th>
<th>Reason</th>
<th>Method</th>
<th>Disposed By</th>
</tr>
</thead>
<tbody>
<?php if (empty($records)): ?>
<tr>
<td colspan="13" class="text-center py-4 text-muted">
<i class="fa fa-info-circle fa-2x mb-2 d-block text-secondary"></i>
No medicine disposal records found for the selected period.
</td>
</tr>
<?php endif; ?>
<?php $sr = 1; foreach ($records as $r): ?>
<tr>
<td><?= $sr++ ?></td>
<td>
<strong class="text-primary font-monospace"><?= esc($r->disposal_no ?? ('EXP#' . $r->id)) ?></strong>
</td>
<td><?= esc(date('d-m-Y', strtotime($r->disposal_date))) ?></td>
<td><strong><?= esc($r->item_name ?? '-') ?></strong></td>
<td><span class="badge bg-secondary font-monospace"><?= esc($r->batch_no ?? '-') ?></span></td>
<td><?= esc($r->expiry_date ? date('d-m-Y', strtotime($r->expiry_date)) : '-') ?></td>
<td><?= esc($r->supplier_name ?? '-') ?></td>
<td class="text-end fw-bold text-danger"><?= esc(number_format((float) ($r->qty ?? 0), 2)) ?></td>
<td class="text-end">₹<?= esc(number_format((float) ($r->purchase_unit_rate ?? 0), 2)) ?></td>
<td class="text-end fw-bold text-danger">₹<?= esc(number_format((float) ($r->total_cost_amount ?? 0), 2)) ?></td>
<td>
<span class="badge bg-danger-subtle text-danger border border-danger-subtle"><?= esc($r->disposal_reason ?? 'Expired') ?></span>
</td>
<td class="small text-muted"><?= esc($r->disposal_method ?? '-') ?></td>
<td class="small"><i class="fa fa-user-circle text-muted me-1"></i><?= esc($r->disposed_by_name ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<?php if (! empty($records)): ?>
<tfoot class="table-light fw-bold">
<tr>
<td colspan="7" class="text-end">Total Summary:</td>
<td class="text-end text-danger"><?= number_format($totalUnits, 2) ?></td>
<td></td>
<td class="text-end text-danger">₹<?= number_format($totalLoss, 2) ?></td>
<td colspan="3"></td>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
<script>
function applyDisposalFilter() {
var fDate = $('#disp_from_date').val();
var tDate = $('#disp_to_date').val();
var q = encodeURIComponent($('#disp_search').val() || '');
var url = '<?= base_url('Medical/disposal_register') ?>?from_date=' + fDate + '&to_date=' + tDate + '&search=' + q;
load_form_div(url, 'medical-main', 'Expired Medicine Disposal Register :Pharmacy');
}
function resetDisposalFilter() {
var url = '<?= base_url('Medical/disposal_register') ?>';
load_form_div(url, 'medical-main', 'Expired Medicine Disposal Register :Pharmacy');
}
</script>

View File

@ -0,0 +1,114 @@
<?php
$records = $records ?? [];
$fromDate = $from_date ?? date('Y-m-01');
$toDate = $to_date ?? date('Y-m-d');
$totalUnits = (float) ($total_units ?? 0);
$totalLoss = (float) ($total_loss ?? 0);
?>
<style>
body { font-family: sans-serif; font-size: 10px; color: #222; }
h2 { margin: 0 0 4px 0; color: #b02a37; }
.subtitle { font-size: 11px; margin-bottom: 8px; color: #555; }
.meta { margin-bottom: 12px; font-size: 10px; color: #444; border-bottom: 1px solid #ddd; padding-bottom: 6px; }
.summary-box { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
.summary-box th, .summary-box td { border: 1px solid #bbb; padding: 5px 8px; }
.summary-box th { background: #f2f2f2; text-align: left; }
.data-table { border-collapse: collapse; width: 100%; }
.data-table th, .data-table td { border: 1px solid #999; padding: 4px 6px; }
.data-table th { background: #343a40; color: #fff; font-size: 9.5px; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.total-row { background: #f8f9fa; font-weight: bold; }
.sign-section { margin-top: 40px; width: 100%; }
.sign-box { width: 33%; text-align: center; font-size: 10px; padding-top: 40px; border-top: 1px dashed #777; }
</style>
<h2>EXPIRED &amp; DAMAGED MEDICINE DISPOSAL REGISTER</h2>
<div class="subtitle"><strong>Audit Certificate for Destruction &amp; Stock Loss (NABH / BMW Rules Compliant)</strong></div>
<div class="meta">
<strong>Report Period:</strong> <?= esc(date('d-m-Y', strtotime($fromDate))) ?> to <?= esc(date('d-m-Y', strtotime($toDate))) ?> &nbsp;|&nbsp;
<strong>Generated On:</strong> <?= esc((string) ($generated_at ?? date('d-m-Y H:i'))) ?>
</div>
<table class="summary-box">
<tr>
<th>Total Disposed Batches</th>
<th>Total Quantity Written Off / Lost</th>
<th>Total Purchase Value (Financial Loss)</th>
</tr>
<tr>
<td><strong><?= count($records) ?></strong> batches</td>
<td style="color:#b02a37;"><strong><?= number_format($totalUnits, 2) ?></strong> units</td>
<td style="color:#0d6efd;"><strong>₹ <?= number_format($totalLoss, 2) ?></strong></td>
</tr>
</table>
<table class="data-table">
<thead>
<tr>
<th style="width: 25px;">#</th>
<th>Voucher No</th>
<th>Date</th>
<th>Medicine Name</th>
<th>Batch No</th>
<th>Exp. Date</th>
<th>Supplier</th>
<th class="text-right">Qty</th>
<th class="text-right">Rate (₹)</th>
<th class="text-right">Loss Amt (₹)</th>
<th>Reason</th>
<th>Disposal Method</th>
<th>Disposed By</th>
</tr>
</thead>
<tbody>
<?php if (empty($records)): ?>
<tr><td colspan="13" class="text-center" style="padding:15px;">No medicine disposal records found for the selected period.</td></tr>
<?php endif; ?>
<?php $sr = 1; foreach ($records as $r): ?>
<tr>
<td class="text-center"><?= $sr++ ?></td>
<td><strong><?= esc($r->disposal_no ?? ('EXP#' . $r->id)) ?></strong></td>
<td><?= esc(date('d-m-Y', strtotime($r->disposal_date))) ?></td>
<td><?= esc($r->item_name ?? '-') ?></td>
<td><?= esc($r->batch_no ?? '-') ?></td>
<td><?= esc($r->expiry_date ? date('d-m-Y', strtotime($r->expiry_date)) : '-') ?></td>
<td><?= esc($r->supplier_name ?? '-') ?></td>
<td class="text-right" style="font-weight:bold; color:#b02a37;"><?= esc(number_format((float) ($r->qty ?? 0), 2)) ?></td>
<td class="text-right"><?= esc(number_format((float) ($r->purchase_unit_rate ?? 0), 2)) ?></td>
<td class="text-right" style="font-weight:bold;"><?= esc(number_format((float) ($r->total_cost_amount ?? 0), 2)) ?></td>
<td><?= esc($r->disposal_reason ?? 'Expired') ?></td>
<td><?= esc($r->disposal_method ?? '-') ?></td>
<td><?= esc($r->disposed_by_name ?? '-') ?></td>
</tr>
<?php endforeach; ?>
<?php if (! empty($records)): ?>
<tr class="total-row">
<td colspan="7" class="text-right">Total Summary:</td>
<td class="text-right" style="color:#b02a37;"><?= number_format($totalUnits, 2) ?></td>
<td></td>
<td class="text-right" style="color:#b02a37;">₹ <?= number_format($totalLoss, 2) ?></td>
<td colspan="3"></td>
</tr>
<?php endif; ?>
</tbody>
</table>
<table class="sign-section">
<tr>
<td class="sign-box">
<strong>Pharmacist / Store Incharge</strong><br>
Signature &amp; Date
</td>
<td style="width: 5%;"></td>
<td class="sign-box">
<strong>Medical Superintendent / Administrator</strong><br>
Verified &amp; Approved
</td>
<td style="width: 5%;"></td>
<td class="sign-box">
<strong>Bio-Medical Waste Handover</strong><br>
Agency Rep. Signature / Stamp
</td>
</tr>
</table>

View File

@ -30,52 +30,94 @@ foreach ($rows as $summaryRow) {
<div class="card">
<div class="card-body pt-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="card-title mb-0">Nearest Expiry / Expired Medicine Stock</h5>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('Medical/expire_stock_pdf') ?>">Print (PDF)</a>
<a class="btn btn-sm btn-outline-secondary" href="javascript:load_form_div('<?= base_url('Medical/master') ?>','medical-main','Master :Pharmacy');">Back to Master</a>
<!-- Top Header & Action Buttons -->
<div class="d-flex flex-wrap justify-content-between align-items-center mb-2 gap-2">
<div>
<h5 class="card-title mb-0">Nearest Expiry / Expired Medicine Stock</h5>
<small class="text-muted">Monitor, Dispose or Return Expired &amp; Near-Expiry Medicine Batches</small>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="load_form_div('<?= base_url('Medical/disposal_register') ?>','medical-main','Disposal Register :Pharmacy');">
<i class="fa fa-archive me-1"></i> Disposal Register
</button>
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('Medical/expire_stock_pdf') ?>">
<i class="fa fa-print me-1"></i> Print (PDF)
</a>
<a class="btn btn-sm btn-outline-secondary" href="javascript:load_form_div('<?= base_url('Medical/master') ?>','medical-main','Master :Pharmacy');">
<i class="fa fa-arrow-left me-1"></i> Back to Master
</a>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge text-bg-danger">Expired</span>
<span class="badge text-bg-warning">Expiring in 30 days</span>
<span class="badge text-bg-info">Expiring in 2 months</span>
<span class="badge text-bg-light border">Above 2 months</span>
<!-- Interactive Category Filters & Search -->
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div class="btn-group btn-group-sm" role="group" id="expiry_filter_buttons">
<button type="button" class="btn btn-dark active" onclick="filterExpiryTable('all', this)">
All <span class="badge bg-light text-dark ms-1" id="badge_count_all"><?= count($rows) ?></span>
</button>
<button type="button" class="btn btn-outline-danger" onclick="filterExpiryTable('expired', this)">
Expired <span class="badge bg-danger ms-1" id="badge_count_expired"><?= esc((string) ($expirySummary['expired'] ?? 0)) ?></span>
</button>
<button type="button" class="btn btn-outline-warning text-dark" onclick="filterExpiryTable('d30', this)">
Expiring in 30 days <span class="badge bg-warning text-dark ms-1" id="badge_count_d30"><?= esc((string) ($expirySummary['d30'] ?? 0)) ?></span>
</button>
<button type="button" class="btn btn-outline-info text-dark" onclick="filterExpiryTable('d60', this)">
Expiring in 2 months <span class="badge bg-info text-dark ms-1" id="badge_count_d60"><?= esc((string) ($expirySummary['d60'] ?? 0)) ?></span>
</button>
<button type="button" class="btn btn-outline-secondary" onclick="filterExpiryTable('later', this)">
Above 2 months <span class="badge bg-secondary ms-1" id="badge_count_later"><?= esc((string) ($expirySummary['later'] ?? 0)) ?></span>
</button>
</div>
<div style="min-width: 260px;">
<input type="text" id="expiry_table_search" class="form-control form-control-sm" placeholder="🔍 Quick search item, supplier, batch..." onkeyup="searchExpiryTable()">
</div>
</div>
<!-- 4 KPI Summary Cards (Clickable) -->
<div class="row g-2 mb-3">
<div class="col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="small text-muted">Expired</div>
<div class="h6 mb-0 text-danger"><?= esc((string) ($expirySummary['expired'] ?? 0)) ?></div>
<div class="border rounded p-2 h-100 bg-light" style="cursor: pointer;" onclick="triggerCardFilter('expired')" title="Click to view Expired medicines">
<div class="small text-muted d-flex justify-content-between">
<span>Expired</span>
<i class="fa fa-filter text-danger"></i>
</div>
<div class="h6 mb-0 text-danger fw-bold" id="card_count_expired"><?= esc((string) ($expirySummary['expired'] ?? 0)) ?></div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="small text-muted">Expiring in 30 days</div>
<div class="h6 mb-0 text-warning"><?= esc((string) ($expirySummary['d30'] ?? 0)) ?></div>
<div class="border rounded p-2 h-100 bg-light" style="cursor: pointer;" onclick="triggerCardFilter('d30')" title="Click to view Expiring in 30 days">
<div class="small text-muted d-flex justify-content-between">
<span>Expiring in 30 days</span>
<i class="fa fa-filter text-warning"></i>
</div>
<div class="h6 mb-0 text-warning fw-bold" id="card_count_d30"><?= esc((string) ($expirySummary['d30'] ?? 0)) ?></div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="small text-muted">Expiring in 2 months</div>
<div class="h6 mb-0 text-info"><?= esc((string) ($expirySummary['d60'] ?? 0)) ?></div>
<div class="border rounded p-2 h-100 bg-light" style="cursor: pointer;" onclick="triggerCardFilter('d60')" title="Click to view Expiring in 2 months">
<div class="small text-muted d-flex justify-content-between">
<span>Expiring in 2 months</span>
<i class="fa fa-filter text-info"></i>
</div>
<div class="h6 mb-0 text-info fw-bold" id="card_count_d60"><?= esc((string) ($expirySummary['d60'] ?? 0)) ?></div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="border rounded p-2 h-100">
<div class="small text-muted">Above 2 months</div>
<div class="h6 mb-0"><?= esc((string) ($expirySummary['later'] ?? 0)) ?></div>
<div class="border rounded p-2 h-100 bg-light" style="cursor: pointer;" onclick="triggerCardFilter('later')" title="Click to view Above 2 months">
<div class="small text-muted d-flex justify-content-between">
<span>Above 2 months</span>
<i class="fa fa-filter text-secondary"></i>
</div>
<div class="h6 mb-0 text-dark fw-bold" id="card_count_later"><?= esc((string) ($expirySummary['later'] ?? 0)) ?></div>
</div>
</div>
</div>
<!-- Expiry Stock Table -->
<div class="table-responsive">
<table class="table table-striped table-bordered table-sm align-middle">
<table class="table table-striped table-bordered table-sm align-middle" id="table_expiry_stock">
<thead>
<tr>
<tr class="table-light">
<th>#</th>
<th>Item</th>
<th>Supplier</th>
@ -86,14 +128,19 @@ foreach ($rows as $summaryRow) {
<th class="text-end">Qty</th>
<th class="text-end">C.Qty</th>
<th>Status</th>
<th class="text-center" style="width: 170px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr id="row_empty_state"><td colspan="11" class="text-center text-muted py-4">No near-expiry or expired medicines found in stock.</td></tr>
<?php endif; ?>
<?php
$srno = 1;
foreach ($rows as $row):
$statusLabel = 'Above 2 months';
$statusClass = '';
$statusCat = 'later';
$expiryDateRaw = (string) ($row->expiry_date ?? '');
$expiryTs = strtotime($expiryDateRaw);
@ -105,31 +152,76 @@ foreach ($rows as $summaryRow) {
if ($daysToExpiry < 0) {
$statusLabel = 'Expired';
$statusClass = 'table-danger';
$statusCat = 'expired';
} elseif ($daysToExpiry <= 30) {
$statusLabel = 'Expiring in 30 days';
$statusClass = 'table-warning';
$statusCat = 'd30';
} elseif ($daysToExpiry <= 60) {
$statusLabel = 'Expiring in 2 months';
$statusClass = 'table-info';
$statusCat = 'd60';
}
}
$itemId = (int) ($row->purchase_item_id ?? $row->id ?? 0);
$itemName = (string) ($row->item_name ?? $row->Item_name ?? '');
$supplierName = (string) ($row->name_supplier ?? '-');
$batchNo = (string) ($row->batch_no ?? '');
$expDisplay = (string) ($row->exp_date ?? $row->exp_date_full ?? '');
$curQty = (float) ($row->cur_qty ?? 0);
$rate = (float) ($row->mrp ?? 0);
$purRate = (float) ($row->purchase_unit_rate ?? 0);
$itemMeta = [
'item_id' => $itemId,
'item_name' => $itemName,
'supplier_name' => $supplierName,
'sid' => (int) ($row->sid ?? 0),
'batch_no' => $batchNo,
'expiry_date' => $expiryDateRaw,
'exp_date_display' => $expDisplay,
'cur_qty' => $curQty,
'rate' => $rate,
'pur_rate' => $purRate,
'days_to_expiry' => $daysToExpiry,
'status_cat' => $statusCat,
];
$searchText = strtolower($itemName . ' ' . $supplierName . ' ' . $batchNo . ' ' . $statusLabel);
?>
<tr class="<?= esc($statusClass) ?>">
<tr class="expiry-data-row <?= esc($statusClass) ?>"
id="row_exp_<?= $itemId ?>"
data-status-category="<?= esc($statusCat) ?>"
data-search-text="<?= esc($searchText) ?>"
data-cur-qty="<?= $curQty ?>">
<td><?= esc((string) $srno++) ?></td>
<td><?= esc($row->item_name ?? $row->Item_name ?? '') ?></td>
<td><?= esc($row->name_supplier ?? '') ?></td>
<td><strong><?= esc($itemName) ?></strong></td>
<td><?= esc($supplierName) ?></td>
<td><?= esc($row->str_date_of_invoice ?? '') ?></td>
<td><?= esc($row->batch_no ?? '') ?></td>
<td><?= esc($row->exp_date ?? '') ?></td>
<td class="text-end"><?= esc(number_format((float) ($row->mrp ?? 0), 2)) ?></td>
<td><span class="badge bg-secondary font-monospace"><?= esc($batchNo) ?></span></td>
<td><?= esc($expDisplay) ?></td>
<td class="text-end"><?= esc(number_format($rate, 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row->tqty ?? 0), 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row->cur_qty ?? 0), 2)) ?></td>
<td class="text-end fw-bold cur-qty-cell"><?= esc(number_format($curQty, 2)) ?></td>
<td>
<?= esc($statusLabel) ?>
<?php if ($daysToExpiry !== null): ?>
<span class="text-muted small">(<?= esc((string) $daysToExpiry) ?> days)</span>
<?php endif; ?>
</td>
<td class="text-center text-nowrap">
<button type="button" class="btn btn-sm btn-outline-danger py-0 px-2 fw-semibold"
onclick='openDisposeModal(<?= htmlspecialchars(json_encode($itemMeta), ENT_QUOTES, 'UTF-8') ?>)'
title="Dispose / Write-Off Expired Medicine">
<i class="fa fa-trash-o me-1"></i> Dispose
</button>
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2 ms-1 fw-semibold"
onclick='openReturnModal(<?= htmlspecialchars(json_encode($itemMeta), ENT_QUOTES, 'UTF-8') ?>)'
title="Return to Supplier (Create Return Invoice)">
<i class="fa fa-reply me-1"></i> Return
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
@ -137,3 +229,336 @@ foreach ($rows as $summaryRow) {
</div>
</div>
</div>
<!-- ========================================== -->
<!-- Modal 1: Dispose / Write-Off Expired Stock -->
<!-- ========================================== -->
<div class="modal fade" id="modal_dispose_stock" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white py-2">
<h6 class="modal-title mb-0"><i class="fa fa-trash-o me-2"></i>Dispose / Write-Off Expired Medicine</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-light border mb-3 p-2 small">
<div><strong>Medicine:</strong> <span id="disp_m_item_name" class="text-dark fw-bold"></span></div>
<div><strong>Batch:</strong> <span id="disp_m_batch" class="font-monospace"></span> | <strong>Exp. Date:</strong> <span id="disp_m_exp" class="text-danger fw-bold"></span></div>
<div><strong>Supplier:</strong> <span id="disp_m_supplier" class="text-muted"></span></div>
<div><strong>Available Stock:</strong> <span id="disp_m_avail" class="badge bg-secondary"></span> units</div>
</div>
<form id="form_dispose_stock" onsubmit="event.preventDefault(); submitStockDisposal();">
<input type="hidden" id="disp_item_id" value="0">
<input type="hidden" id="disp_max_qty" value="0">
<input type="hidden" id="disp_row_id" value="">
<div class="mb-3">
<label class="form-label small fw-semibold">Disposal Quantity <span class="text-danger">*</span></label>
<div class="input-group input-group-sm">
<input type="number" step="any" min="0.01" class="form-control" id="disp_qty" required>
<button type="button" class="btn btn-outline-secondary" onclick="$('#disp_qty').val($('#disp_max_qty').val())">Max (All)</button>
</div>
<div class="form-text small">Quantity to permanently remove from stock inventory (recorded under loss/disposal).</div>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Reason for Disposal <span class="text-danger">*</span></label>
<select class="form-select form-select-sm" id="disp_reason" required>
<option value="Expired / Outdated" selected>Expired / Outdated</option>
<option value="Damaged / Broken Ampoule or Vial">Damaged / Broken Ampoule or Vial</option>
<option value="Cold Chain Failure / Spoilage">Cold Chain Failure / Spoilage</option>
<option value="Opened / Contaminated">Opened / Contaminated</option>
<option value="Batch Quality Recall">Batch Quality Recall</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Destruction / Disposal Method</label>
<select class="form-select form-select-sm" id="disp_method">
<option value="Bio-Medical Waste Handover (Incineration)" selected>Bio-Medical Waste Handover (Incineration)</option>
<option value="Autoclave / Shredding">Autoclave / Shredding</option>
<option value="Chemical Treatment / Neutralization">Chemical Treatment / Neutralization</option>
<option value="Handover to Authorized Waste Agency">Handover to Authorized Waste Agency</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Remarks / Audit Note</label>
<input type="text" class="form-control form-control-sm" id="disp_remarks" placeholder="e.g. Expired on shelf, authorized by MS">
</div>
</form>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-sm" id="btn_confirm_dispose" onclick="submitStockDisposal()">
<i class="fa fa-trash me-1"></i> Confirm &amp; Remove from Stock
</button>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- Modal 2: Return Medicine to Supplier -->
<!-- ========================================== -->
<div class="modal fade" id="modal_return_stock" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white py-2">
<h6 class="modal-title mb-0"><i class="fa fa-reply me-2"></i>Return Medicine to Supplier</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-light border mb-3 p-2 small">
<div><strong>Medicine:</strong> <span id="ret_m_item_name" class="text-dark fw-bold"></span></div>
<div><strong>Batch:</strong> <span id="ret_m_batch" class="font-monospace"></span> | <strong>Exp. Date:</strong> <span id="ret_m_exp" class="text-primary fw-bold"></span></div>
<div><strong>Supplier:</strong> <span id="ret_m_supplier" class="badge bg-primary-subtle text-primary border border-primary-subtle"></span></div>
<div><strong>Available Stock:</strong> <span id="ret_m_avail" class="badge bg-secondary"></span> units</div>
</div>
<form id="form_return_stock" onsubmit="event.preventDefault(); submitStockReturn();">
<input type="hidden" id="ret_item_id" value="0">
<input type="hidden" id="ret_max_qty" value="0">
<div class="mb-2">
<label class="form-label small fw-semibold">Return Quantity <span class="text-danger">*</span></label>
<div class="input-group input-group-sm">
<input type="number" step="any" min="0.01" class="form-control" id="ret_qty" required>
<button type="button" class="btn btn-outline-secondary" onclick="$('#ret_qty').val($('#ret_max_qty').val())">Max (All)</button>
</div>
<div class="form-text small">This will create or add to today's draft Purchase Return Invoice for this supplier.</div>
</div>
</form>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_confirm_return" onclick="submitStockReturn()">
<i class="fa fa-plus me-1"></i> Add &amp; Open Return Invoice
</button>
</div>
</div>
</div>
</div>
<script>
var currentCategoryFilter = 'all';
function filterExpiryTable(category, btnElement) {
currentCategoryFilter = category;
$('#expiry_filter_buttons button').removeClass('active').addClass('btn-outline-danger btn-outline-warning btn-outline-info btn-outline-secondary');
if (category === 'all') {
$(btnElement).removeClass('btn-outline-secondary').addClass('btn-dark active');
} else {
$(btnElement).removeClass('btn-outline-danger btn-outline-warning btn-outline-info btn-outline-secondary').addClass('active');
}
applyCombinedFilters();
}
function triggerCardFilter(category) {
var btn = $('#expiry_filter_buttons button[onclick*="' + category + '"]');
if (btn.length) {
filterExpiryTable(category, btn[0]);
}
}
function searchExpiryTable() {
applyCombinedFilters();
}
function applyCombinedFilters() {
var search = ($('#expiry_table_search').val() || '').toLowerCase().trim();
var visibleCount = 0;
$('.expiry-data-row').each(function () {
var $row = $(this);
var rowCat = $row.data('status-category') || '';
var rowText = $row.data('search-text') || '';
var matchesCategory = (currentCategoryFilter === 'all' || rowCat === currentCategoryFilter);
var matchesSearch = (search === '' || rowText.indexOf(search) !== -1);
if (matchesCategory && matchesSearch) {
$row.show();
visibleCount++;
} else {
$row.hide();
}
});
if (visibleCount === 0) {
if ($('#row_no_matches').length === 0) {
$('#table_expiry_stock tbody').append('<tr id="row_no_matches"><td colspan="11" class="text-center text-muted py-4">No matching medicines found for the selected filter.</td></tr>');
}
$('#row_no_matches').show();
} else {
$('#row_no_matches').hide();
}
}
function openDisposeModal(item) {
$('#disp_item_id').val(item.item_id);
$('#disp_max_qty').val(item.cur_qty);
$('#disp_row_id').val('row_exp_' + item.item_id);
$('#disp_m_item_name').text(item.item_name);
$('#disp_m_batch').text(item.batch_no || '-');
$('#disp_m_exp').text(item.exp_date_display || item.expiry_date || '-');
$('#disp_m_supplier').text(item.supplier_name || '-');
$('#disp_m_avail').text(item.cur_qty);
$('#disp_qty').val(item.cur_qty).attr('max', item.cur_qty);
$('#disp_remarks').val('');
$('#disp_reason').val('Expired / Outdated');
var modal = new bootstrap.Modal(document.getElementById('modal_dispose_stock'));
modal.show();
}
function submitStockDisposal() {
var itemId = parseInt($('#disp_item_id').val() || '0', 10);
var qty = parseFloat($('#disp_qty').val() || 0);
var maxQty = parseFloat($('#disp_max_qty').val() || 0);
var reason = $('#disp_reason').val();
var method = $('#disp_method').val();
var remarks = $('#disp_remarks').val();
if (itemId <= 0 || qty <= 0) {
notify('error', 'Please Attention', 'Please enter a valid disposal quantity');
return;
}
if (qty > maxQty) {
notify('error', 'Please Attention', 'Disposal quantity cannot exceed available stock (' + maxQty + ')');
return;
}
var payload = {
item_id: itemId,
qty: qty,
reason: reason,
method: method,
remarks: remarks
};
var csrfName = $('input[name="<?= csrf_token() ?>"]').attr('name');
var csrfValue = $('input[name="<?= csrf_token() ?>"]').val();
if (csrfName && csrfValue) {
payload[csrfName] = csrfValue;
}
$('#btn_confirm_dispose').prop('disabled', true).html('<i class="fa fa-spinner fa-spin me-1"></i> Disposing...');
$.post('<?= base_url('Medical/dispose_expired_stock') ?>', payload, function (data) {
$('#btn_confirm_dispose').prop('disabled', false).html('<i class="fa fa-trash me-1"></i> Confirm &amp; Remove from Stock');
if (!data || data.status !== 1) {
notify('error', 'Please Attention', data && data.msg ? data.msg : 'Unable to dispose stock');
return;
}
notify('success', 'Disposal Successful', data.msg);
// Hide modal
var modalEl = document.getElementById('modal_dispose_stock');
var modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
// Update row in table
var rowId = $('#disp_row_id').val();
var remainingQty = parseFloat(data.remaining_qty || 0);
if (remainingQty <= 0) {
$('#' + rowId).fadeOut(400, function () {
$(this).remove();
applyCombinedFilters();
});
} else {
$('#' + rowId).find('.cur-qty-cell').text(remainingQty.toFixed(2));
$('#' + rowId).data('cur-qty', remainingQty);
}
}, 'json').fail(function () {
$('#btn_confirm_dispose').prop('disabled', false).html('<i class="fa fa-trash me-1"></i> Confirm &amp; Remove from Stock');
notify('error', 'Error', 'Failed to communicate with server');
});
}
function openReturnModal(item) {
if (!item.sid || parseInt(item.sid, 10) <= 0) {
notify('warning', 'Notice', 'No supplier linked with this batch. Cannot create supplier return invoice.');
return;
}
$('#ret_item_id').val(item.item_id);
$('#ret_max_qty').val(item.cur_qty);
$('#ret_m_item_name').text(item.item_name);
$('#ret_m_batch').text(item.batch_no || '-');
$('#ret_m_exp').text(item.exp_date_display || item.expiry_date || '-');
$('#ret_m_supplier').text(item.supplier_name || '-');
$('#ret_m_avail').text(item.cur_qty);
$('#ret_qty').val(item.cur_qty).attr('max', item.cur_qty);
var modal = new bootstrap.Modal(document.getElementById('modal_return_stock'));
modal.show();
}
function submitStockReturn() {
var itemId = parseInt($('#ret_item_id').val() || '0', 10);
var qty = parseFloat($('#ret_qty').val() || 0);
var maxQty = parseFloat($('#ret_max_qty').val() || 0);
if (itemId <= 0 || qty <= 0) {
notify('error', 'Please Attention', 'Please enter a valid return quantity');
return;
}
if (qty > maxQty) {
notify('error', 'Please Attention', 'Return quantity cannot exceed available stock (' + maxQty + ')');
return;
}
var payload = {
item_id: itemId,
qty: qty
};
var csrfName = $('input[name="<?= csrf_token() ?>"]').attr('name');
var csrfValue = $('input[name="<?= csrf_token() ?>"]').val();
if (csrfName && csrfValue) {
payload[csrfName] = csrfValue;
}
$('#btn_confirm_return').prop('disabled', true).html('<i class="fa fa-spinner fa-spin me-1"></i> Creating Return...');
$.post('<?= base_url('Medical/quick_create_purchase_return') ?>', payload, function (data) {
$('#btn_confirm_return').prop('disabled', false).html('<i class="fa fa-plus me-1"></i> Add &amp; Open Return Invoice');
if (!data || data.status !== 1) {
notify('error', 'Please Attention', data && data.msg ? data.msg : 'Unable to create purchase return');
return;
}
notify('success', 'Return Invoice Created', data.msg);
// Hide modal
var modalEl = document.getElementById('modal_return_stock');
var modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
// Open the purchase return invoice edit screen directly!
if (data.inv_id) {
load_form_div('<?= base_url('Medical_backpanel/PurchaseReturnInvoiceEdit') ?>/' + data.inv_id, 'medical-main', 'Purchase Return Invoice Edit');
}
}, 'json').fail(function () {
$('#btn_confirm_return').prop('disabled', false).html('<i class="fa fa-plus me-1"></i> Add &amp; Open Return Invoice');
notify('error', 'Error', 'Failed to communicate with server');
});
}
</script>

View File

@ -71,7 +71,7 @@
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/expire_stock') ?>','medical-main','Expiry Medicine :Pharmacy');">Expiry Medicine</a>
</li>
<li class="list-group-item px-0">
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/master_report/lost-medicine') ?>','medical-main','Lost Medicine :Pharmacy');">Lost Medicine</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/disposal_register') ?>','medical-main','Expired &amp; Damaged Medicine Disposal Register :Pharmacy');">Expired / Lost Medicine Disposal Register</a>
</li>
<li class="list-group-item px-0">
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Short_medicine') ?>','medical-main','Short Medicine List :Pharmacy');">Short Medicine List</a>

View File

@ -74,8 +74,21 @@ function remove_custom_item() {
var itemid = parseInt($('#l_ssno').val() || '0', 10);
var inv_id = <?= (int) ($invoice->id ?? 0) ?>;
if (itemid <= 0 || rqty <= 0) {
notify('error', 'Please Attention', 'Choose product and quantity');
if (itemid <= 0) {
notify('error', 'Please Attention', 'Please search and select a product first');
$('#input_drug').focus();
return;
}
if (rqty <= 0) {
notify('error', 'Please Attention', 'Please enter a valid return quantity');
$('#input_product_qty').focus();
return;
}
var availQty = parseFloat($('#hid_c_qty').val() || 0);
if (availQty > 0 && rqty > availQty) {
notify('error', 'Please Attention', 'Return quantity (' + rqty + ') cannot exceed available stock (' + availQty + ')');
$('#input_product_qty').focus();
return;
}
@ -106,8 +119,11 @@ function remove_custom_item() {
$('#input_product_unit_rate').val('');
$('#l_ssno').val(0);
$('#input_drug').val('');
$('#input_product_qty').val('0');
$('#input_product_qty').val('');
$('#stock_product_qty').html('');
$('#input_supplier_info').html('');
$('#input_expiry_status').html('');
$('#selected_item_preview').hide();
$('#purchase_id').val('');
notify('success', 'Please Attention', data.msg_text || 'Item Added');

View File

@ -1,10 +1,10 @@
<div class="row g-2">
<div class="col-md-8">
<label class="form-label" for="input_drug">Product Search</label>
<input class="form-control form-control-sm" name="input_drug" id="input_drug" placeholder="Like Item Code, Item Name" type="text">
<label class="form-label fw-semibold" for="input_drug">Product Search</label>
<input class="form-control form-control-sm" name="input_drug" id="input_drug" placeholder="Type Item Code, Item Name, Batch..." type="text" autocomplete="off">
</div>
<div class="col-md-4 d-flex align-items-end">
<span class="text-muted" id="input_product_code"></span>
<span class="text-muted small" id="input_product_code"></span>
</div>
</div>
@ -12,40 +12,46 @@
<div class="col-12">
<input type="hidden" id="l_ssno" name="l_ssno" value="0">
<input type="hidden" id="purchase_id" name="purchase_id" value="0">
<p class="text-danger mb-1">
<span class="text-success" id="input_product_name"></span>
<span class="text-info" id="input_batch"></span>
<span class="text-primary" id="input_product_mrp"></span>
<span class="text-warning" id="stock_product_qty"></span>
</p>
<div class="p-2 border rounded bg-light mb-2" id="selected_item_preview" style="min-height: 38px; display: none;">
<div class="d-flex flex-wrap gap-2 align-items-center" style="font-size: 0.85rem;">
<span class="text-success fw-bold" id="input_product_name"></span>
<span class="text-secondary" id="input_batch"></span>
<span class="text-primary" id="input_product_mrp"></span>
<span class="text-dark" id="stock_product_qty"></span>
<span id="input_supplier_info"></span>
<span id="input_expiry_status"></span>
</div>
</div>
</div>
</div>
<div class="row g-2">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label">Unit Rate</label>
<label class="form-label small fw-semibold">Unit Rate (P. Rate)</label>
<input class="form-control form-control-sm" name="input_product_unit_rate" id="input_product_unit_rate" placeholder="Unit Rate" autocomplete="off">
</div>
<div class="col-md-3">
<label class="form-label">Qty</label>
<input class="form-control form-control-sm" name="input_product_qty" id="input_product_qty" placeholder="Qty" type="text" value="0" autocomplete="off">
<label class="form-label small fw-semibold">Return Qty</label>
<input class="form-control form-control-sm" name="input_product_qty" id="input_product_qty" placeholder="Qty" type="number" min="1" step="any" value="" autocomplete="off">
<input type="hidden" id="hid_c_qty" name="hid_c_qty" value="0">
</div>
<div class="col-md-3">
<label class="form-label">Batch No.</label>
<input class="form-control form-control-sm" name="input_batch_no" id="input_batch_no" placeholder="000000" type="text">
<label class="form-label small fw-semibold">Batch No.</label>
<input class="form-control form-control-sm" name="input_batch_no" id="input_batch_no" placeholder="Batch No." type="text">
</div>
<div class="col-md-3">
<label class="form-label">Exp.Date</label>
<label class="form-label small fw-semibold">Exp.Date</label>
<input class="form-control form-control-sm" name="input_expiry_dt" id="input_expiry_dt" type="date" autocomplete="off">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-primary" onclick="remove_custom_item()">Add</button>
<div class="col-12 text-end mt-2">
<button type="button" class="btn btn-primary btn-sm px-4" onclick="remove_custom_item()"><i class="fa fa-plus me-1"></i> Add to Return Invoice</button>
</div>
</div>
<script>
(function () {
var currentSuppId = <?= (int) ($supp_id ?? 0) ?>;
$('#input_drug').autocomplete({
source: function (request, response) {
$.getJSON('<?= base_url('Medical_backpanel/get_drug') ?>', request, function (data) {
@ -55,20 +61,100 @@
minLength: 1,
autofocus: true,
select: function (event, ui) {
$('#input_product_code').html('| Product Code: ' + (ui.item.l_item_code || ''));
$('#input_product_name').html('Name: ' + (ui.item.value || ''));
$('#l_ssno').val(ui.item.l_ss_no || 0);
$('#input_batch').html(' | Batch No.: ' + (ui.item.l_Batch || '') + ' | Exp.Dt: ' + (ui.item.l_Expiry || ''));
$('#input_product_mrp').html(' | MRP: ' + (ui.item.l_mrp || '') + ' | Unit Rate: ' + (ui.item.l_unit_rate || ''));
$('#input_product_unit_rate').val(ui.item.l_unit_rate || '');
$('#purchase_id').val(ui.item.l_purchase_id || '');
$('#hid_c_qty').val(ui.item.l_c_qty || 0);
$('#input_product_qty').val(ui.item.l_c_qty || 0);
$('#input_batch_no').val(ui.item.l_Batch || '');
$('#input_expiry_dt').val(ui.item.l_Expiry || '');
$('#stock_product_qty').html(' | Qty: ' + (ui.item.l_c_qty || 0) + ' | Pak: ' + (ui.item.l_packing || ''));
var item = ui.item || {};
var purRate = parseFloat(item.l_pur_unit_rate || 0);
if (purRate <= 0) {
purRate = parseFloat(item.l_unit_rate || 0);
}
var expText = item.l_expiry_display || item.l_Expiry || '-';
$('#selected_item_preview').show();
$('#input_product_code').html('<strong>Code:</strong> ' + (item.l_item_code || '-'));
$('#input_product_name').html('<strong>' + (item.value || '') + '</strong>');
$('#l_ssno').val(item.l_ss_no || 0);
$('#purchase_id').val(item.l_purchase_id || 0);
$('#input_batch').html(' | <strong>Batch:</strong> ' + (item.l_Batch || '-') + ' | <strong>Exp:</strong> ' + expText);
$('#input_product_mrp').html(' | <strong>MRP:</strong> ₹' + (item.l_mrp || '-') + ' | <strong>P. Rate:</strong> ₹' + purRate.toFixed(2));
$('#stock_product_qty').html(' | <strong>Avail Stock:</strong> ' + (item.l_c_qty || 0));
// Supplier display
if (item.supplier_name && item.supplier_name !== '-') {
var itemSuppId = parseInt(item.supplier_id || '0', 10);
var isSameSupp = (currentSuppId > 0 && itemSuppId > 0 && currentSuppId === itemSuppId);
var badgeClass = isSameSupp ? 'badge bg-success-subtle text-success border border-success-subtle' : 'badge bg-secondary-subtle text-dark border';
$('#input_supplier_info').html(' | <strong>Supplier:</strong> <span class="' + badgeClass + '"><i class="fa fa-truck"></i> ' + item.supplier_name + (isSameSupp ? ' (Same)' : '') + '</span>');
} else {
$('#input_supplier_info').html('');
}
// Expiry status badge
if (item.is_expired == 1 || parseFloat(item.expiry_alert || '999') <= 0) {
$('#input_expiry_status').html('<span class="badge bg-danger text-white"><i class="fa fa-ban"></i> Expired</span>');
} else if (parseFloat(item.expiry_alert || '999') <= 3) {
$('#input_expiry_status').html('<span class="badge bg-warning text-dark"><i class="fa fa-clock-o"></i> Near Expiry</span>');
} else {
$('#input_expiry_status').html('');
}
$('#input_product_unit_rate').val(purRate > 0 ? purRate.toFixed(2) : (item.l_unit_rate || ''));
$('#hid_c_qty').val(item.l_c_qty || 0);
$('#input_product_qty').val(item.l_c_qty || '');
$('#input_batch_no').val(item.l_Batch || '');
$('#input_expiry_dt').val(item.l_Expiry || '');
setTimeout(function() {
$('#input_product_qty').focus().select();
}, 60);
}
});
}).autocomplete("instance")._renderItem = function (ul, item) {
var isExpired = (parseInt(item.is_expired || '0', 10) === 1 || parseFloat(item.expiry_alert || '999') <= 0);
var isNearExpiry = (!isExpired && parseFloat(item.expiry_alert || '999') <= 3);
var bulb = (item.l_new_stock || '0') > 0 ? '💡 ' : '';
var label = item.label || '';
// Supplier badge
var itemSuppId = parseInt(item.supplier_id || '0', 10);
var suppHtml = '';
if (item.supplier_name && item.supplier_name !== '-') {
if (currentSuppId > 0 && itemSuppId > 0 && currentSuppId === itemSuppId) {
suppHtml = '<span class="badge bg-success-subtle text-success border border-success-subtle me-1" style="font-size:0.75rem;"><i class="fa fa-check-circle"></i> Supplier: ' + item.supplier_name + ' (Same)</span>';
} else {
suppHtml = '<span class="badge bg-secondary-subtle text-dark border border-secondary-subtle me-1" style="font-size:0.75rem;"><i class="fa fa-truck"></i> Supplier: ' + item.supplier_name + '</span>';
}
}
// Expiry text/badge
var expDisplay = item.l_expiry_display || item.l_Expiry || '-';
var expHtml = '';
if (isExpired) {
var expText = item.desc || '<span style="color: red; font-weight: bold;">Expired</span>';
expHtml = '<span class="badge bg-danger-subtle text-danger border border-danger-subtle fw-bold" style="font-size:0.75rem;"><i class="fa fa-exclamation-triangle"></i> ' + expText + ' [Exp: ' + expDisplay + '] &bull; Returnable</span>';
} else if (isNearExpiry) {
expHtml = '<span class="badge bg-warning-subtle text-dark border border-warning-subtle fw-semibold" style="font-size:0.75rem;"><i class="fa fa-clock-o"></i> ' + (item.desc || ('Expiring in ' + item.expiry_alert + ' Mo')) + ' [Exp: ' + expDisplay + ']</span>';
} else {
expHtml = '<span class="text-muted small" style="font-size:0.75rem;"><i class="fa fa-calendar"></i> Exp: ' + expDisplay + '</span>';
}
// List item - ALWAYS SELECTABLE (no ui-state-disabled)
var $li = $('<li>')
.addClass('border-bottom py-1')
.css({
'cursor': 'pointer',
'border-left': isExpired ? '4px solid #dc3545' : (isNearExpiry ? '4px solid #ffc107' : '4px solid transparent'),
'background-color': isExpired ? '#fff5f5' : (isNearExpiry ? '#fffdf0' : '')
});
var html = '<div class="px-2 py-1">' +
'<div class="fw-semibold text-dark" style="font-size:0.875rem;">' + bulb + label + '</div>' +
'<div class="d-flex flex-wrap align-items-center gap-1 mt-1">' +
suppHtml +
expHtml +
'</div>' +
'</div>';
return $li.append(html).appendTo(ul);
};
$('#input_batch_no').autocomplete({
source: function (request, response) {