fix(pharmacy): resolve CSRF, schema auto-migration, and error handling for mobile draft basket

This commit is contained in:
DevSoftTech Server 2026-10-27 00:23:10 +05:30
parent 9f6d9d3799
commit 97ca466d2f
4 changed files with 412 additions and 160 deletions

View File

@ -94,6 +94,14 @@ class Filters extends BaseFilters
'stt-proxy/*',
'billing/charges',
'billing/charges/*',
'Medical/mobile_save_draft',
'Medical/mobile_save_audit',
'Medical/stock_audit_reconcile',
'Medical/mobile_*',
'medical/mobile_save_draft',
'medical/mobile_save_audit',
'medical/stock_audit_reconcile',
'medical/mobile_*',
],
],
// 'honeypot',

View File

@ -55,18 +55,29 @@ $routes->get('Medical/invoice_print/(:num)', 'Medical::invoice_print/$1');
$routes->get('Medical/invoice_print/(:num)/(:num)', 'Medical::invoice_print/$1/$2');
$routes->get('Medical/Invoice_Med_Draft', 'Medical::Invoice_Med_Draft');
$routes->get('Medical/mobile', 'Medical::mobile');
$routes->get('medical/mobile', 'Medical::mobile');
$routes->get('app/central-pharmacy', 'Medical::mobile');
$routes->get('app/main-pharmacy', 'Medical::mobile');
$routes->get('Medical/mobile_patient_search', 'Medical::mobilePatientSearch');
$routes->get('medical/mobile_patient_search', 'Medical::mobilePatientSearch');
$routes->get('Medical/mobile_patient_prescription/(:num)', 'Medical::mobilePatientPrescription/$1');
$routes->get('medical/mobile_patient_prescription/(:num)', 'Medical::mobilePatientPrescription/$1');
$routes->get('Medical/mobile_medicine_search', 'Medical::mobileMedicineSearch');
$routes->get('medical/mobile_medicine_search', 'Medical::mobileMedicineSearch');
$routes->post('Medical/mobile_save_draft', 'Medical::mobileSaveDraft');
$routes->post('medical/mobile_save_draft', 'Medical::mobileSaveDraft');
$routes->get('Medical/mobile_recent_drafts', 'Medical::mobileRecentDrafts');
$routes->get('medical/mobile_recent_drafts', 'Medical::mobileRecentDrafts');
$routes->get('Medical/mobile_audit_racks', 'Medical::mobileAuditRacks');
$routes->get('medical/mobile_audit_racks', 'Medical::mobileAuditRacks');
$routes->get('Medical/mobile_audit_items', 'Medical::mobileAuditItems');
$routes->get('medical/mobile_audit_items', 'Medical::mobileAuditItems');
$routes->post('Medical/mobile_save_audit', 'Medical::mobileSaveAudit');
$routes->post('medical/mobile_save_audit', 'Medical::mobileSaveAudit');
$routes->get('Medical/stock_audit_report', 'Medical::stockAuditReport');
$routes->get('medical/stock_audit_report', 'Medical::stockAuditReport');
$routes->post('Medical/stock_audit_reconcile', 'Medical::stockAuditReconcile');
$routes->post('medical/stock_audit_reconcile', 'Medical::stockAuditReconcile');
$routes->get('Medical/list_med_inv/(:num)', 'Medical::list_med_inv/$1');
$routes->get('Medical/list_med_orginv/(:num)', 'Medical::list_med_orginv/$1');
$routes->get('Medical/list_med_orginv/(:num)/(:num)', 'Medical::list_med_orginv/$1/$2');

View File

@ -15491,6 +15491,8 @@ class Medical extends BaseController
public function mobile()
{
$this->ensureMobileStoreColumns();
$hospitalSetting = [];
if ($this->db->tableExists('hospital_setting')) {
$hospitalSetting = $this->db->table('hospital_setting')->get()->getRowArray() ?: [];
@ -15503,6 +15505,7 @@ class Medical extends BaseController
'hospitalSetting' => $hospitalSetting,
'sessionUser' => $sessionUser,
'userId' => $userId,
'footerVersion' => '26.117',
'pageTitle' => 'Central Pharmacy Mobile & Picker'
]);
}
@ -15696,170 +15699,200 @@ class Medical extends BaseController
public function mobileSaveDraft()
{
$json = $this->request->getJSON(true) ?: $this->request->getPost();
if (empty($json) && !empty($this->request->getBody())) {
$json = json_decode($this->request->getBody(), true) ?: [];
}
$this->ensureMobileStoreColumns();
$items = $json['items'] ?? [];
if (empty($items) || !is_array($items)) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Please add at least one medicine item to basket.']);
}
$patientId = (int)($json['patient_id'] ?? 0);
$uhid = trim((string)($json['uhid'] ?? ''));
$patientName = trim((string)($json['patient_name'] ?? 'Walk-in Customer'));
$mobile = trim((string)($json['mobile'] ?? ''));
$ipdId = (int)($json['ipd_id'] ?? 0);
$ipdCode = trim((string)($json['ipd_code'] ?? ''));
$bedNo = trim((string)($json['bed_no'] ?? ''));
$opdId = (int)($json['opd_id'] ?? 0);
$opdCode = trim((string)($json['opd_code'] ?? ''));
$docName = trim((string)($json['doc_name'] ?? ''));
$pickerName = trim((string)($json['picker_name'] ?? (session('name') ?: 'Mobile Picker')));
$invFields = $this->db->getFieldNames('invoice_med_master') ?? [];
$setIfExists = static function (array &$arr, array $fields, string $col, $val): void {
if (in_array($col, $fields, true)) {
$arr[$col] = $val;
try {
$json = (method_exists($this->request, 'getJSON') ? $this->request->getJSON(true) : null) ?: $this->request->getPost();
if (empty($json) && !empty($this->request->getBody())) {
$json = json_decode($this->request->getBody(), true) ?: [];
}
};
$header = [];
$setIfExists($header, $invFields, 'store_id', 1);
$setIfExists($header, $invFields, 'inv_date', date('Y-m-d'));
$setIfExists($header, $invFields, 'inv_insert_datetime', date('Y-m-d H:i:s'));
$setIfExists($header, $invFields, 'customer_type', $ipdId > 0 ? 2 : 1);
$setIfExists($header, $invFields, 'patient_id', $patientId);
$setIfExists($header, $invFields, 'patient_code', $uhid);
$setIfExists($header, $invFields, 'inv_name', $patientName);
$setIfExists($header, $invFields, 'inv_phone_number', $mobile);
$setIfExists($header, $invFields, 'ipd_id', $ipdId);
$setIfExists($header, $invFields, 'ipd_code', $ipdCode);
$setIfExists($header, $invFields, 'bed_no', $bedNo);
$setIfExists($header, $invFields, 'doc_name', $docName);
$setIfExists($header, $invFields, 'draft_source', 'mobile_picker');
$setIfExists($header, $invFields, 'picker_name', $pickerName);
$setIfExists($header, $invFields, 'prepared_by', $pickerName);
$setIfExists($header, $invFields, 'bill_final', 0);
$setIfExists($header, $invFields, 'invoice_status', 0);
$setIfExists($header, $invFields, 'log', 'Mobile draft basket created by ' . $pickerName . ' at ' . date('d-m-Y H:i:s'));
$items = $json['items'] ?? [];
if (empty($items) || !is_array($items)) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Please add at least one medicine item to basket.']);
}
$this->db->table('invoice_med_master')->insert($header);
$invoiceId = (int)$this->db->insertID();
$patientId = (int)($json['patient_id'] ?? 0);
$uhid = trim((string)($json['uhid'] ?? ''));
$patientName = trim((string)($json['patient_name'] ?? 'Walk-in Customer'));
$mobile = trim((string)($json['mobile'] ?? ''));
$ipdId = (int)($json['ipd_id'] ?? 0);
$ipdCode = trim((string)($json['ipd_code'] ?? ''));
$bedNo = trim((string)($json['bed_no'] ?? ''));
$opdId = (int)($json['opd_id'] ?? 0);
$opdCode = trim((string)($json['opd_code'] ?? ''));
$docName = trim((string)($json['doc_name'] ?? ''));
$pickerName = trim((string)($json['picker_name'] ?? (session('name') ?: 'Mobile Picker')));
if ($invoiceId <= 0) {
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to initialize draft bill.']);
$invFields = $this->db->getFieldNames('invoice_med_master') ?? [];
$setIfExists = static function (array &$arr, array $fields, string $col, $val): void {
if (in_array($col, $fields, true)) {
$arr[$col] = $val;
}
};
$header = [];
$setIfExists($header, $invFields, 'store_id', 1);
$setIfExists($header, $invFields, 'inv_date', date('Y-m-d'));
$setIfExists($header, $invFields, 'inv_insert_datetime', date('Y-m-d H:i:s'));
$setIfExists($header, $invFields, 'customer_type', $ipdId > 0 ? 2 : 1);
$setIfExists($header, $invFields, 'patient_id', $patientId);
$setIfExists($header, $invFields, 'patient_code', $uhid);
$setIfExists($header, $invFields, 'inv_name', $patientName);
$setIfExists($header, $invFields, 'inv_phone_number', $mobile);
$setIfExists($header, $invFields, 'ipd_id', $ipdId);
$setIfExists($header, $invFields, 'ipd_code', $ipdCode);
$setIfExists($header, $invFields, 'bed_no', $bedNo);
$setIfExists($header, $invFields, 'doc_name', $docName);
$setIfExists($header, $invFields, 'draft_source', 'mobile_picker');
$setIfExists($header, $invFields, 'picker_name', $pickerName);
$setIfExists($header, $invFields, 'prepared_by', $pickerName);
$setIfExists($header, $invFields, 'bill_final', 0);
$setIfExists($header, $invFields, 'invoice_status', 0);
$setIfExists($header, $invFields, 'log', 'Mobile draft basket created by ' . $pickerName . ' at ' . date('d-m-Y H:i:s'));
$this->db->table('invoice_med_master')->insert($header);
$invoiceId = (int)$this->db->insertID();
if ($invoiceId <= 0) {
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to initialize draft bill.']);
}
$pid = str_pad(substr((string)$invoiceId, -7, 7), 7, '0', STR_PAD_LEFT);
$invoiceCode = 'M' . date('ym') . $pid;
if (in_array('inv_med_code', $invFields, true)) {
$this->db->table('invoice_med_master')->where('id', $invoiceId)->update(['inv_med_code' => $invoiceCode]);
}
$itemFields = $this->db->getFieldNames('inv_med_item') ?? [];
$totalGross = 0;
foreach ($items as $it) {
$itemCode = (int)($it['item_code'] ?? 0);
$batchId = (int)($it['batch_id'] ?? 0);
$qty = max(1, (int)($it['qty'] ?? 1));
$rate = (float)($it['rate'] ?? ($it['selling_unit_rate'] ?? ($it['mrp'] ?? 0)));
$mrp = (float)($it['mrp'] ?? $rate);
$packing = (float)($it['packing'] ?? 1);
$lineTotal = round($qty * $rate, 2);
$totalGross += $lineTotal;
$taxBreakup = $this->calculateInvoiceItemTaxBreakup($lineTotal, 6.0, 6.0, 0.0);
$line = [];
$setIfExists($line, $itemFields, 'inv_med_id', $invoiceId);
$setIfExists($line, $itemFields, 'item_code', $itemCode);
$setIfExists($line, $itemFields, 'store_stock_id', $batchId);
$setIfExists($line, $itemFields, 'purchase_item_id', $batchId);
$setIfExists($line, $itemFields, 'item_Name', (string)($it['item_name'] ?? 'Medicine'));
$setIfExists($line, $itemFields, 'formulation', (string)($it['formulation'] ?? ''));
$setIfExists($line, $itemFields, 'batch_no', (string)($it['batch_no'] ?? ''));
$setIfExists($line, $itemFields, 'expiry', (string)($it['expiry_date'] ?? ''));
$setIfExists($line, $itemFields, 'qty', $qty);
$setIfExists($line, $itemFields, 'price', $rate);
$setIfExists($line, $itemFields, 'mrp', $mrp);
$setIfExists($line, $itemFields, 'packing', $packing);
$setIfExists($line, $itemFields, 'amount', $lineTotal);
$setIfExists($line, $itemFields, 'tamount', $lineTotal);
$setIfExists($line, $itemFields, 'TaxableAmount', $taxBreakup['taxable']);
$setIfExists($line, $itemFields, 'CGST_per', 6.0);
$setIfExists($line, $itemFields, 'CGST', $taxBreakup['cgst']);
$setIfExists($line, $itemFields, 'SGST_per', 6.0);
$setIfExists($line, $itemFields, 'SGST', $taxBreakup['sgst']);
$setIfExists($line, $itemFields, 'update_by_remark', 'Mobile Picker: ' . $pickerName);
$this->db->table('inv_med_item')->insert($line);
}
$this->recalculateInvoiceTotals($invoiceId);
return $this->response->setJSON([
'status' => 1,
'message' => "Draft Bill #{$invoiceCode} created and sent to billing counter.",
'draft_id' => $invoiceId,
'draft_code' => $invoiceCode,
'total_items' => count($items),
'total_amount' => round($totalGross, 2)
]);
} catch (\Throwable $e) {
log_message('error', 'mobileSaveDraft error: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'status' => 0,
'message' => 'Server error creating draft: ' . $e->getMessage()
]);
}
$pid = str_pad(substr((string)$invoiceId, -7, 7), 7, '0', STR_PAD_LEFT);
$invoiceCode = 'M' . date('ym') . $pid;
if (in_array('inv_med_code', $invFields, true)) {
$this->db->table('invoice_med_master')->where('id', $invoiceId)->update(['inv_med_code' => $invoiceCode]);
}
$itemFields = $this->db->getFieldNames('inv_med_item') ?? [];
$totalGross = 0;
foreach ($items as $it) {
$itemCode = (int)($it['item_code'] ?? 0);
$batchId = (int)($it['batch_id'] ?? 0);
$qty = max(1, (int)($it['qty'] ?? 1));
$rate = (float)($it['rate'] ?? ($it['selling_unit_rate'] ?? ($it['mrp'] ?? 0)));
$mrp = (float)($it['mrp'] ?? $rate);
$packing = (float)($it['packing'] ?? 1);
$lineTotal = round($qty * $rate, 2);
$totalGross += $lineTotal;
$taxBreakup = $this->calculateInvoiceItemTaxBreakup($lineTotal, 6.0, 6.0, 0.0);
$line = [];
$setIfExists($line, $itemFields, 'inv_med_id', $invoiceId);
$setIfExists($line, $itemFields, 'item_code', $itemCode);
$setIfExists($line, $itemFields, 'store_stock_id', $batchId);
$setIfExists($line, $itemFields, 'purchase_item_id', $batchId);
$setIfExists($line, $itemFields, 'item_Name', (string)($it['item_name'] ?? 'Medicine'));
$setIfExists($line, $itemFields, 'formulation', (string)($it['formulation'] ?? ''));
$setIfExists($line, $itemFields, 'batch_no', (string)($it['batch_no'] ?? ''));
$setIfExists($line, $itemFields, 'expiry', (string)($it['expiry_date'] ?? ''));
$setIfExists($line, $itemFields, 'qty', $qty);
$setIfExists($line, $itemFields, 'price', $rate);
$setIfExists($line, $itemFields, 'mrp', $mrp);
$setIfExists($line, $itemFields, 'packing', $packing);
$setIfExists($line, $itemFields, 'amount', $lineTotal);
$setIfExists($line, $itemFields, 'tamount', $lineTotal);
$setIfExists($line, $itemFields, 'TaxableAmount', $taxBreakup['taxable']);
$setIfExists($line, $itemFields, 'CGST_per', 6.0);
$setIfExists($line, $itemFields, 'CGST', $taxBreakup['cgst']);
$setIfExists($line, $itemFields, 'SGST_per', 6.0);
$setIfExists($line, $itemFields, 'SGST', $taxBreakup['sgst']);
$setIfExists($line, $itemFields, 'update_by_remark', 'Mobile Picker: ' . $pickerName);
$this->db->table('inv_med_item')->insert($line);
}
$this->recalculateInvoiceTotals($invoiceId);
return $this->response->setJSON([
'status' => 1,
'message' => "Draft Bill #{$invoiceCode} created and sent to billing counter.",
'draft_id' => $invoiceId,
'draft_code' => $invoiceCode,
'total_items' => count($items),
'total_amount' => round($totalGross, 2)
]);
}
public function mobileRecentDrafts()
{
$builder = $this->db->table('invoice_med_master m');
$builder->select('
m.id,
m.inv_med_code,
m.patient_code AS uhid,
m.inv_name AS patient_name,
m.ipd_id,
m.ipd_code,
m.bed_no,
m.net_amount,
m.bill_final,
m.draft_source,
m.picker_name,
m.inv_date,
m.inv_insert_datetime,
COUNT(i.id) AS total_items
', false);
$builder->join('inv_med_item i', 'i.inv_med_id = m.id', 'left');
$builder->groupStart();
$builder->where('m.draft_source', 'mobile_picker');
$builder->orWhere('m.inv_date', date('Y-m-d'));
$builder->groupEnd();
$builder->groupBy('m.id');
$builder->orderBy('m.id', 'DESC');
$builder->limit(30);
$this->ensureMobileStoreColumns();
$drafts = $builder->get()->getResultArray();
$list = [];
try {
$invFields = $this->db->getFieldNames('invoice_med_master') ?? [];
$hasDraftSource = in_array('draft_source', $invFields, true);
$hasPickerName = in_array('picker_name', $invFields, true);
$hasBedNo = in_array('bed_no', $invFields, true);
foreach ($drafts as $d) {
$isFinal = (int)($d['bill_final'] ?? 0) === 1;
$timeStr = !empty($d['inv_insert_datetime']) ? date('h:i A', strtotime($d['inv_insert_datetime'])) : date('d-M', strtotime($d['inv_date']));
$builder = $this->db->table('invoice_med_master m');
$builder->select('
m.id,
m.inv_med_code,
m.patient_code AS uhid,
m.inv_name AS patient_name,
m.ipd_id,
m.ipd_code,
' . ($hasBedNo ? 'm.bed_no' : "'' AS bed_no") . ',
m.net_amount,
m.bill_final,
' . ($hasDraftSource ? 'm.draft_source' : "'' AS draft_source") . ',
' . ($hasPickerName ? 'm.picker_name' : "'' AS picker_name") . ',
m.inv_date,
m.inv_insert_datetime,
(SELECT COUNT(*) FROM inv_med_item i WHERE i.inv_med_id = m.id) AS total_items
', false);
$list[] = [
'id' => (int)$d['id'],
'draft_code' => (string)($d['inv_med_code'] ?: ('M' . $d['id'])),
'uhid' => (string)($d['uhid'] ?: '-'),
'patient_name' => (string)($d['patient_name'] ?: 'Patient'),
'ipd_tag' => !empty($d['ipd_id']) ? ('IPD #' . ($d['ipd_code'] ?: $d['ipd_id']) . (!empty($d['bed_no']) ? ' [' . $d['bed_no'] . ']' : '')) : 'OPD',
'net_amount' => (float)($d['net_amount'] ?? 0),
'total_items' => (int)($d['total_items'] ?? 0),
'picker_name' => (string)($d['picker_name'] ?: 'Picker'),
'time_str' => $timeStr,
'status' => $isFinal ? 'BILLED' : 'DRAFT',
'status_label' => $isFinal ? 'Billed at Counter' : 'At Billing Counter'
];
$builder->groupStart();
if ($hasDraftSource) {
$builder->where('m.draft_source', 'mobile_picker');
$builder->orWhere('m.inv_date', date('Y-m-d'));
} else {
$builder->where('m.inv_date', date('Y-m-d'));
}
$builder->groupEnd();
$builder->orderBy('m.id', 'DESC');
$builder->limit(30);
$drafts = $builder->get()->getResultArray();
$list = [];
foreach ($drafts as $d) {
$isFinal = (int)($d['bill_final'] ?? 0) === 1;
$timeStr = !empty($d['inv_insert_datetime']) ? date('h:i A', strtotime($d['inv_insert_datetime'])) : date('d-M', strtotime($d['inv_date']));
$list[] = [
'id' => (int)$d['id'],
'draft_code' => (string)($d['inv_med_code'] ?: ('M' . $d['id'])),
'uhid' => (string)($d['uhid'] ?: '-'),
'patient_name' => (string)($d['patient_name'] ?: 'Patient'),
'ipd_tag' => !empty($d['ipd_id']) ? ('IPD #' . ($d['ipd_code'] ?: $d['ipd_id']) . (!empty($d['bed_no']) ? ' [' . $d['bed_no'] . ']' : '')) : 'OPD',
'net_amount' => (float)($d['net_amount'] ?? 0),
'total_items' => (int)($d['total_items'] ?? 0),
'picker_name' => (string)($d['picker_name'] ?: 'Picker'),
'time_str' => $timeStr,
'status' => $isFinal ? 'BILLED' : 'DRAFT',
'status_label' => $isFinal ? 'Billed at Counter' : 'At Billing Counter'
];
}
return $this->response->setJSON(['status' => 1, 'drafts' => $list]);
} catch (\Throwable $e) {
log_message('error', 'mobileRecentDrafts error: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'status' => 0,
'message' => 'Error retrieving drafts: ' . $e->getMessage(),
'drafts' => []
]);
}
return $this->response->setJSON(['status' => 1, 'drafts' => $list]);
}
public function mobileAuditRacks()
@ -15981,7 +16014,9 @@ class Medical extends BaseController
public function mobileSaveAudit()
{
$json = $this->request->getJSON(true) ?: $this->request->getPost();
$this->ensureMobileStoreColumns();
$json = (method_exists($this->request, 'getJSON') ? $this->request->getJSON(true) : null) ?: $this->request->getPost();
if (empty($json) && !empty($this->request->getBody())) {
$json = json_decode($this->request->getBody(), true) ?: [];
}
@ -16167,4 +16202,154 @@ class Medical extends BaseController
return $this->response->setJSON(['status' => 1, 'message' => 'Stock inventory adjusted and audit marked as RECONCILED.']);
}
protected function ensureMobileStoreColumns(): void
{
static $ensured = false;
if ($ensured) {
return;
}
try {
$forge = \Config\Database::forge();
$fields = $this->db->getFieldNames('invoice_med_master') ?? [];
$missing = [];
if (!in_array('draft_source', $fields, true)) {
$missing['draft_source'] = [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
'default' => null,
];
}
if (!in_array('picker_name', $fields, true)) {
$missing['picker_name'] = [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
'default' => null,
];
}
if (!in_array('bed_no', $fields, true)) {
$missing['bed_no'] = [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
'default' => null,
];
}
if (!empty($missing)) {
$forge->addColumn('invoice_med_master', $missing);
}
if (!$this->db->tableExists('mst_stock_audit')) {
$forge->addField([
'audit_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'store_id' => [
'type' => 'INT',
'constraint' => 11,
'default' => 1,
],
'item_id' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'batch_id' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'batch_no' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
],
'expiry_date' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
],
'audit_type' => [
'type' => 'VARCHAR',
'constraint' => 50,
'default' => 'OK',
],
'system_qty' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'physical_qty' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'variation_qty' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'rate' => [
'type' => 'DECIMAL',
'constraint' => '12,2',
'default' => 0.00,
],
'total_value' => [
'type' => 'DECIMAL',
'constraint' => '12,2',
'default' => 0.00,
],
'remarks' => [
'type' => 'TEXT',
'null' => true,
],
'rack_no' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
],
'shelf_no' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
],
'reconciled_status' => [
'type' => 'VARCHAR',
'constraint' => 30,
'default' => 'PENDING',
],
'conducted_by' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'conducted_by_name' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$forge->addKey('audit_id', true);
$forge->addKey(['store_id', 'item_id', 'batch_id']);
$forge->createTable('mst_stock_audit', true);
}
$ensured = true;
} catch (\Throwable $e) {
log_message('error', 'ensureMobileStoreColumns error: ' . $e->getMessage());
}
}
}

View File

@ -384,6 +384,13 @@
</div>
<!-- System & License Footer -->
<footer class="text-center py-3 mt-4 mb-4 text-muted" style="font-size: 11px; border-top: 1px solid #e2e8f0;">
&copy; 2017 - 2026 <strong><a href="https://www.e-atria.in" target="_blank" rel="noopener" class="text-decoration-none text-primary">E-Atria</a></strong>. All Rights Reserved |
<a href="<?= base_url('software-use-license.html') ?>" target="_blank" rel="noopener" class="text-decoration-none text-secondary">Software Use License</a> |
Version: <?= esc($footerVersion ?? '26.117') ?>
</footer>
</div>
<!-- Floating Bottom Bar for Basket -->
@ -400,7 +407,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="showBasketDrawer();" title="View Picked Items">
<i class="bi bi-list-check"></i> Review
</button>
<button type="button" class="btn btn-primary btn-sm fw-bold px-3 shadow-sm" id="btn_send_draft" onclick="sendDraftToCounter();">
<button type="button" class="btn btn-primary btn-sm fw-bold px-3 shadow-sm btn-send-draft-action" id="btn_send_draft" onclick="sendDraftToCounter();">
<i class="bi bi-send-fill me-1"></i> Send to Counter
</button>
</div>
@ -424,7 +431,7 @@
<div class="small text-muted">Total Estimated Amount</div>
<h5 class="fw-bold text-primary mb-0" id="basket_modal_total">₹0.00</h5>
</div>
<button type="button" class="btn btn-primary fw-bold" onclick="sendDraftToCounter();">
<button type="button" class="btn btn-primary fw-bold btn-send-draft-action" id="btn_modal_confirm_draft" onclick="sendDraftToCounter();">
<i class="bi bi-send-fill me-1"></i> Confirm &amp; Send Draft
</button>
</div>
@ -465,7 +472,19 @@
// =========================================================================
// STATE & GLOBAL VARIABLES
// =========================================================================
var baseUrl = '<?= base_url() ?>';
var baseUrl = '<?= rtrim(base_url(), '/') ?>/';
var csrfHeader = '<?= csrf_header() ?>';
var csrfTokenName = '<?= csrf_token() ?>';
var csrfHash = '<?= csrf_hash() ?>';
// Global AJAX setup: inject CSRF token header & X-Requested-With
jQuery.ajaxSetup({
headers: {
'<?= csrf_header() ?>': '<?= csrf_hash() ?>',
'X-Requested-With': 'XMLHttpRequest'
}
});
var selectedPatient = null;
var cartItems = [];
var html5QrCodeScanner = null;
@ -809,9 +828,10 @@
picker_name: operatorName,
items: cartItems
};
payload[csrfTokenName] = csrfHash;
var btn = jQuery('#btn_send_draft');
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Sending...');
var btns = jQuery('.btn-send-draft-action');
btns.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Sending Draft...');
jQuery.ajax({
url: baseUrl + 'Medical/mobile_save_draft',
@ -820,7 +840,10 @@
contentType: 'application/json',
dataType: 'json',
success: function(res) {
btn.prop('disabled', false).html('<i class="bi bi-send-fill me-1"></i> Send to Counter');
btns.prop('disabled', false);
jQuery('#btn_send_draft').html('<i class="bi bi-send-fill me-1"></i> Send to Counter');
jQuery('#btn_modal_confirm_draft').html('<i class="bi bi-send-fill me-1"></i> Confirm &amp; Send Draft');
if (res.status === 1) {
var m = bootstrap.Modal.getInstance(document.getElementById('basketModal'));
if (m) m.hide();
@ -837,9 +860,19 @@
alert(res.message || 'Failed to save draft bill.');
}
},
error: function() {
btn.prop('disabled', false).html('<i class="bi bi-send-fill me-1"></i> Send to Counter');
alert("Failed to communicate with hospital server. Please check WiFi/intranet connection.");
error: function(xhr, status, error) {
btns.prop('disabled', false);
jQuery('#btn_send_draft').html('<i class="bi bi-send-fill me-1"></i> Send to Counter');
jQuery('#btn_modal_confirm_draft').html('<i class="bi bi-send-fill me-1"></i> Confirm &amp; Send Draft');
var errDetail = "HTTP " + xhr.status + " (" + (xhr.statusText || error || 'Connection Error') + ")";
try {
var parsed = JSON.parse(xhr.responseText);
if (parsed && parsed.message) {
errDetail += "\n\nServer message: " + parsed.message;
}
} catch(e) {}
alert("Failed to communicate with hospital server.\n\n" + errDetail + "\n\nPlease check WiFi/intranet connection or contact IT support.");
}
});
}
@ -996,6 +1029,7 @@
shelf_no: shelfNo,
audited_by_name: operatorName
};
payload[csrfTokenName] = csrfHash;
jQuery.ajax({
url: baseUrl + 'Medical/mobile_save_audit',
@ -1014,6 +1048,10 @@
} else {
alert(res.message || 'Error recording audit.');
}
},
error: function(xhr, status, error) {
var errDetail = "HTTP " + xhr.status + " (" + (xhr.statusText || error || 'Connection Error') + ")";
alert("Failed to save audit count.\n\n" + errDetail);
}
});
}
@ -1059,6 +1097,16 @@
});
container.html(html);
},
error: function(xhr, status, error) {
var errDetail = "HTTP " + xhr.status + " (" + (xhr.statusText || error || 'Connection Error') + ")";
container.html('<div class="text-center py-4 text-danger">' +
'<i class="bi bi-exclamation-triangle fs-3 d-block mb-1"></i>' +
'Failed to load recent drafts: ' + escapeHtml(errDetail) + '<br>' +
'<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="loadRecentDrafts();">' +
'<i class="bi bi-arrow-repeat"></i> Retry' +
'</button>' +
'</div>');
}
});
}