fix(pharmacy): resolve CSRF, schema auto-migration, and error handling for mobile draft basket
This commit is contained in:
parent
9f6d9d3799
commit
97ca466d2f
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
© 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 & 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 & 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 & 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>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user