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,7 +15699,10 @@ class Medical extends BaseController
public function mobileSaveDraft()
{
$json = $this->request->getJSON(true) ?: $this->request->getPost();
$this->ensureMobileStoreColumns();
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) ?: [];
}
@ -15807,10 +15813,25 @@ class Medical extends BaseController
'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()
]);
}
}
public function mobileRecentDrafts()
{
$this->ensureMobileStoreColumns();
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);
$builder = $this->db->table('invoice_med_master m');
$builder->select('
m.id,
@ -15819,21 +15840,25 @@ class Medical extends BaseController
m.inv_name AS patient_name,
m.ipd_id,
m.ipd_code,
m.bed_no,
' . ($hasBedNo ? 'm.bed_no' : "'' AS bed_no") . ',
m.net_amount,
m.bill_final,
m.draft_source,
m.picker_name,
' . ($hasDraftSource ? 'm.draft_source' : "'' AS draft_source") . ',
' . ($hasPickerName ? 'm.picker_name' : "'' AS picker_name") . ',
m.inv_date,
m.inv_insert_datetime,
COUNT(i.id) AS total_items
(SELECT COUNT(*) FROM inv_med_item i WHERE i.inv_med_id = m.id) AS total_items
', false);
$builder->join('inv_med_item i', 'i.inv_med_id = m.id', 'left');
$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->groupBy('m.id');
$builder->orderBy('m.id', 'DESC');
$builder->limit(30);
@ -15860,6 +15885,14 @@ class Medical extends BaseController
}
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' => []
]);
}
}
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>');
}
});
}