From 0aa0edbbcecab7cf48ee4439e8bbf78df62f6cc6 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 28 Apr 2026 12:01:53 +0530 Subject: [PATCH] Fix OPD confirm payment flow and DataTable initialization - Opd.php: mode-0 (zero amount) no longer inserts payment_history row and skips HealthPlix sync for faster confirm - Opd.php: added spid-based idempotency for cash/card to prevent duplicate payment_history rows on retry - Opd.php: invoice view hides 0-amount payment_history rows - opd_appointment_list.php: fix isDataTable call (DataTable -> dataTable) - jquery.dataTables.min.js: replace non-functional shim with real DataTables 1.13.8 (supports ext.search, isDataTable, rows() API) --- app/Controllers/Opd.php | 76 ++++-- app/Views/billing/opd_appointment_list.php | 2 +- .../datatables/jquery.dataTables.min.js | 222 +----------------- 3 files changed, 61 insertions(+), 239 deletions(-) diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php index 642c735..3db9fbb 100644 --- a/app/Controllers/Opd.php +++ b/app/Controllers/Opd.php @@ -1672,7 +1672,10 @@ class Opd extends BaseController $sql = "select id,payment_date,insert_time,payment_mode,credit_debit,amount,remark,card_tran_id,update_by, (case payment_mode when 0 then 'Zero Amount' when 1 then 'Cash' when 2 then 'Bank/Online' when 3 then 'Return Cash' when 4 then 'Bank Return' else 'Other' end) as payment_mode_str - from payment_history where payof_type=1 and payof_id=" . (int) $opdId . " order by id desc"; + from payment_history + where payof_type=1 and payof_id=" . (int) $opdId . " + and not (payment_mode=0 and coalesce(amount,0)=0) + order by id desc"; $query = $this->db->query($sql); $data['payment_history_rows'] = $query->getResult(); @@ -4368,6 +4371,7 @@ class Opd extends BaseController $paidAmount = (float) ($chkPayment[0]->opd_pay ?? 0); $pendingAmt = (float) $opdMaster[0]->opd_fee_amount - $paidAmount; + $alreadyPaid = (int) ($opdMaster[0]->payment_status ?? 0) === 1; $payRemark = ''; if (!empty($opdMaster[0]->opd_discount) && (float) $opdMaster[0]->opd_discount > 0) { @@ -4383,28 +4387,29 @@ class Opd extends BaseController $paymentModel = new PaymentModel(); $opdModel = new OpdModel(); - if ($mode === 0) { - $paydata = [ - 'payment_mode' => '0', - 'payof_type' => '1', - 'payof_id' => $oid, - 'payof_code' => $opdMaster[0]->opd_code, - 'credit_debit' => '0', - 'amount' => $opdMaster[0]->opd_fee_amount, - 'payment_date' => date('Y-m-d H:i:s'), - 'remark' => $payRemark, - 'update_by' => $userNameInfo . ' [' . $userId . ']', - 'update_by_id' => $userId, - 'insert_code' => $spid, - ]; + if ($alreadyPaid && in_array($mode, [1, 2, 4], true)) { + return $this->response->setJSON([ + 'update' => 1, + 'showcontent' => (string) ($opdMaster[0]->payment_mode_desc ?? 'Paid'), + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } - $insertId = $paymentModel->insertPayment($paydata); + if ($mode === 0) { + if ($alreadyPaid && $pendingAmt <= 0) { + return $this->response->setJSON([ + 'update' => 1, + 'showcontent' => 'No Cost', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } $data = [ 'payment_mode' => '0', 'payment_status' => '1', 'payment_mode_desc' => 'No Cost', - 'payment_id' => $insertId, 'confirm_pay_opd' => date('Y-m-d H:i:s'), 'prepared_by_id' => $userId, ]; @@ -4412,7 +4417,7 @@ class Opd extends BaseController $this->db->table('opd_master')->where('opd_id', $oid)->update($data); $this->autoCreateOpdQueue($oid); - $this->syncPaidOpdToHealthplix($oid); + // Keep zero-amount confirm fast; user can trigger HealthPlix sync manually from invoice. return $this->response->setJSON([ 'update' => 1, @@ -4437,7 +4442,22 @@ class Opd extends BaseController 'insert_code' => $spid, ]; - $insertId = $paymentModel->insertPayment($paydata); + $existingPayment = null; + if ($spid !== '') { + $existingPayment = $this->db->table('payment_history') + ->select('id') + ->where('payof_type', 1) + ->where('payof_id', $oid) + ->where('insert_code', $spid) + ->orderBy('id', 'DESC') + ->get(1) + ->getRow(); + } + + $insertId = (int) ($existingPayment->id ?? 0); + if ($insertId <= 0) { + $insertId = $paymentModel->insertPayment($paydata); + } $data = [ 'payment_mode' => '1', @@ -4485,9 +4505,25 @@ class Opd extends BaseController 'pay_bank_id' => (int) $this->request->getPost('cbo_pay_type'), 'card_tran_id' => $cardTran, 'update_by_id' => $userId, + 'insert_code' => $spid, ]; - $insertId = $paymentModel->insertPayment($paydata); + $existingPayment = null; + if ($spid !== '') { + $existingPayment = $this->db->table('payment_history') + ->select('id') + ->where('payof_type', 1) + ->where('payof_id', $oid) + ->where('insert_code', $spid) + ->orderBy('id', 'DESC') + ->get(1) + ->getRow(); + } + + $insertId = (int) ($existingPayment->id ?? 0); + if ($insertId <= 0) { + $insertId = $paymentModel->insertPayment($paydata); + } $data = [ 'payment_mode' => '2', diff --git a/app/Views/billing/opd_appointment_list.php b/app/Views/billing/opd_appointment_list.php index 2c31d3c..f475b10 100644 --- a/app/Views/billing/opd_appointment_list.php +++ b/app/Views/billing/opd_appointment_list.php @@ -327,7 +327,7 @@ $mergedGroups = [ return null; } - if ($.fn.DataTable.isDataTable('#opdAllTable')) { + if ($.fn.dataTable.isDataTable('#opdAllTable')) { $('#opdAllTable').DataTable().destroy(); } diff --git a/public/assets/vendor/datatables/jquery.dataTables.min.js b/public/assets/vendor/datatables/jquery.dataTables.min.js index c4cb164..b6d9aa8 100644 --- a/public/assets/vendor/datatables/jquery.dataTables.min.js +++ b/public/assets/vendor/datatables/jquery.dataTables.min.js @@ -1,218 +1,4 @@ -(function($){ - if(!$ || !$.fn){return;} - if($.fn.DataTable){return;} - - function SimpleDataTable($table, options){ - this.$table = $table; - this.options = $.extend(true, { order: [[0, "asc"]], ajax: null, columnDefs: [], paging: true, pageLength: 10, lengthMenu: [10, 25, 50, 100], lengthChange: true, info: true }, options || {}); - this.state = { draw: 1, start: 0, length: this.options.pageLength || 10, order: this.options.order, columns: [] }; - this.lastResponse = null; - this.initColumns(); - this.load(); - } - - SimpleDataTable.prototype.initColumns = function(){ - var count = this.$table.find("thead").first().find("th").length || 0; - this.state.columns = []; - for(var i=0;i= 0; - }else{ - hit = (targets === colIndex); - } - if(hit && typeof def.render === "function"){ - var renderData = (def.data === null) ? rowData : cellData; - return def.render(renderData, "display", rowData, { row: rowIndex, col: colIndex }); - } - } - return cellData; - }; - - SimpleDataTable.prototype.render = function(rows){ - var $tbody = this.$table.find("tbody"); - $tbody.empty(); - for(var r=0;r"); - for(var c=0;c"); - $td.html(cellHtml == null ? "" : cellHtml); - $tr.append($td); - } - $tbody.append($tr); - } - }; - - SimpleDataTable.prototype.ensureControls = function(){ - if(this.$controls){return;} - this.$controls = $("
"); - this.$length = $("
"); - this.$info = $("
"); - this.$paginate = $("
"); - this.$controls.append(this.$length, this.$info, this.$paginate); - this.$table.after(this.$controls); - }; - - SimpleDataTable.prototype.renderLengthMenu = function(){ - if(!this.options.lengthChange){ - this.$length.empty(); - return; - } - var self = this; - var $select = $(""); - var menu = this.options.lengthMenu || [10, 25, 50, 100]; - for(var i=0;i").attr("value", val).text(val); - if(parseInt(val, 10) === parseInt(this.state.length, 10)){ - $opt.attr("selected", "selected"); - } - $select.append($opt); - } - this.$length.empty().append("Show ").append($select).append(" entries"); - $select.off("change").on("change", function(){ - var v = parseInt($(this).val(), 10); - self.state.length = isNaN(v) ? self.state.length : v; - self.state.start = 0; - self.load(); - }); - }; - - SimpleDataTable.prototype.renderInfo = function(totalFiltered){ - if(!this.options.info){ - this.$info.empty(); - return; - } - var start = this.state.start + 1; - var end = Math.min(this.state.start + this.state.length, totalFiltered); - if(totalFiltered === 0){ - this.$info.text("Showing 0 to 0 of 0 entries"); - }else{ - this.$info.text("Showing " + start + " to " + end + " of " + totalFiltered + " entries"); - } - }; - - SimpleDataTable.prototype.renderPager = function(totalFiltered){ - if(!this.options.paging){ - this.$paginate.empty(); - return; - } - var self = this; - var totalPages = Math.ceil(totalFiltered / this.state.length) || 1; - var currentPage = Math.floor(this.state.start / this.state.length) + 1; - this.$paginate.empty(); - - var $prev = $(""); - $prev.prop("disabled", currentPage <= 1); - $prev.on("click", function(){ - if(currentPage > 1){ - self.state.start = (currentPage - 2) * self.state.length; - self.load(); - } - }); - - var $next = $(""); - $next.prop("disabled", currentPage >= totalPages); - $next.on("click", function(){ - if(currentPage < totalPages){ - self.state.start = currentPage * self.state.length; - self.load(); - } - }); - - var $label = $("").text("Page " + currentPage + " of " + totalPages); - - this.$paginate.append($prev, $label, $next); - }; - - SimpleDataTable.prototype.load = function(){ - var self = this; - var ajax = this.options.ajax || {}; - if(!ajax.url){return;} - - var requestData = this.buildRequest(); - if(ajax.data){ - if(typeof ajax.data === "function"){ - requestData = ajax.data(requestData) || requestData; - }else{ - for(var k in ajax.data){ - if(Object.prototype.hasOwnProperty.call(ajax.data, k)){ - requestData[k] = ajax.data[k]; - } - } - } - } - - $.ajax({ - url: ajax.url, - type: ajax.type || "GET", - data: requestData, - dataType: "json" - }).done(function(resp){ - if(resp && Array.isArray(resp.data)){ - self.lastResponse = resp; - self.render(resp.data); - self.ensureControls(); - var totalFiltered = parseInt(resp.recordsFiltered, 10); - if(isNaN(totalFiltered)){ - totalFiltered = parseInt(resp.recordsTotal, 10); - } - if(isNaN(totalFiltered)){ - totalFiltered = resp.data.length; - } - self.renderLengthMenu(); - self.renderInfo(totalFiltered); - self.renderPager(totalFiltered); - } - }).fail(function(){ - if(typeof ajax.error === "function"){ajax.error();} - }); - - this.state.draw += 1; - }; - - SimpleDataTable.prototype.columns = function(index){ - var self = this; - return { - search: function(value){ - if(self.state.columns[index]){ - self.state.columns[index].search.value = value; - } - return self; - } - }; - }; - - SimpleDataTable.prototype.draw = function(){ - this.state.start = 0; - this.load(); - }; - - $.fn.DataTable = function(options){ - if(!this.length){return null;} - return new SimpleDataTable(this, options); - }; -})(window.jQuery); +/*! DataTables 1.13.8 + * ©2008-2023 SpryMedia Ltd - datatables.net/license + */ +!function(n){"use strict";var a;"function"==typeof define&&define.amd?define(["jquery"],function(t){return n(t,window,document)}):"object"==typeof exports?(a=require("jquery"),"undefined"==typeof window?module.exports=function(t,e){return t=t||window,e=e||a(t),n(e,t,t.document)}:module.exports=n(a,window,window.document)):window.DataTable=n(jQuery,window,document)}(function(P,j,v,H){"use strict";function d(t){var e=parseInt(t,10);return!isNaN(e)&&isFinite(t)?e:null}function l(t,e,n){var a=typeof t,r="string"==a;return"number"==a||"bigint"==a||!!h(t)||(e&&r&&(t=$(t,e)),n&&r&&(t=t.replace(q,"")),!isNaN(parseFloat(t))&&isFinite(t))}function a(t,e,n){var a;return!!h(t)||(h(a=t)||"string"==typeof a)&&!!l(t.replace(V,"").replace(/