From 2958f2e9b6cf1250432a566f4ad68ea02307a857 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 7 Apr 2026 11:23:20 +0530 Subject: [PATCH] fix: queue no per-doctor-per-day isolation and stop auto-refresh on digest poll - Queue No (Opd.php::autoCreateOpdQueue, Opd_prescription.php::opd_prescription_save and ::ensurePrescriptionSession): changed MAX(queue_no) query to JOIN through opd_master.doc_id instead of trusting opd_prescription.doc_id, ensuring queue numbering is strictly per-doctor per-day regardless of stale doc_id values in opd_prescription rows. - OPD appointment list: replaced auto-reload-on-digest-change with a manual 'Refresh' button. When the 8s digest poll detects new data the button turns yellow with a 'New' badge; the page only reloads when the user explicitly clicks it, preventing disruptive mid-session reloads." --- app/Controllers/Opd.php | 15 +-- app/Controllers/Opd_prescription.php | 28 ++--- app/Views/billing/opd_appointment_list.php | 118 ++++++--------------- 3 files changed, 56 insertions(+), 105 deletions(-) diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php index 7ff4ad1..3857d1e 100644 --- a/app/Controllers/Opd.php +++ b/app/Controllers/Opd.php @@ -4201,12 +4201,15 @@ class Opd extends BaseController $insert['session_id'] = 0; } if (in_array('queue_no', $fields, true)) { - $maxQueue = $this->db->table('opd_prescription') - ->selectMax('queue_no', 'max_queue') - ->where('date_opd_visit', date('Y-m-d')) - ->where('doc_id', (int) ($opdRow->doc_id ?? 0)) - ->get() - ->getRow(); + // JOIN through opd_master to guarantee per-doctor isolation + // (opd_prescription.doc_id may be stale on re-assignments) + $maxQueue = $this->db->query( + 'SELECT COALESCE(MAX(pr.queue_no), 0) AS max_queue' + . ' FROM opd_prescription pr' + . ' INNER JOIN opd_master om ON om.opd_id = pr.opd_id' + . ' WHERE pr.date_opd_visit = ? AND om.doc_id = ?', + [date('Y-m-d'), (int) ($opdRow->doc_id ?? 0)] + )->getRow(); $insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index cb92e21..a65156a 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -482,12 +482,14 @@ class Opd_prescription extends BaseController } if (in_array('queue_no', $fields, true) && !isset($allowedData['queue_no'])) { - $maxQueue = $this->db->table('opd_prescription') - ->selectMax('queue_no', 'max_queue') - ->where('date_opd_visit', date('Y-m-d')) - ->where('doc_id', (int) ($opdRow->doc_id ?? 0)) - ->get() - ->getRow(); + // JOIN through opd_master to guarantee per-doctor isolation + $maxQueue = $this->db->query( + 'SELECT COALESCE(MAX(pr.queue_no), 0) AS max_queue' + . ' FROM opd_prescription pr' + . ' INNER JOIN opd_master om ON om.opd_id = pr.opd_id' + . ' WHERE pr.date_opd_visit = ? AND om.doc_id = ?', + [date('Y-m-d'), (int) ($opdRow->doc_id ?? 0)] + )->getRow(); $allowedData['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } @@ -6008,12 +6010,14 @@ class Opd_prescription extends BaseController $insert['session_id'] = 0; } if (in_array('queue_no', $fields, true)) { - $maxQueue = $this->db->table('opd_prescription') - ->selectMax('queue_no', 'max_queue') - ->where('date_opd_visit', date('Y-m-d')) - ->where('doc_id', (int) ($opdRow->doc_id ?? 0)) - ->get() - ->getRow(); + // JOIN through opd_master to guarantee per-doctor isolation + $maxQueue = $this->db->query( + 'SELECT COALESCE(MAX(pr.queue_no), 0) AS max_queue' + . ' FROM opd_prescription pr' + . ' INNER JOIN opd_master om ON om.opd_id = pr.opd_id' + . ' WHERE pr.date_opd_visit = ? AND om.doc_id = ?', + [date('Y-m-d'), (int) ($opdRow->doc_id ?? 0)] + )->getRow(); $insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } diff --git a/app/Views/billing/opd_appointment_list.php b/app/Views/billing/opd_appointment_list.php index a4dacf8..685bba2 100644 --- a/app/Views/billing/opd_appointment_list.php +++ b/app/Views/billing/opd_appointment_list.php @@ -57,9 +57,14 @@ $mergedGroups = [ Cancelled -
- - +
+ +
+ + +
@@ -471,14 +476,28 @@ $mergedGroups = [ } if (liveDigest !== String(resp.digest || '')) { - mergeLiveRowsOrReload(); - return; + // Instead of auto-reloading, show notification on the Refresh button + signalRefreshAvailable(); } }).always(function() { livePollInFlight = false; }); } + function signalRefreshAvailable() { + var $btn = $('#btn_manual_refresh'); + if (!$btn.hasClass('btn-warning')) { + $btn.removeClass('btn-outline-secondary').addClass('btn-warning text-dark'); + $btn.html('↻ Refresh New'); + } + } + + function clearRefreshSignal() { + var $btn = $('#btn_manual_refresh'); + $btn.removeClass('btn-warning text-dark').addClass('btn-outline-secondary'); + $btn.html('↻ Refresh'); + } + function updateQueueCell($row, queueNo) { var qNo = parseInt(queueNo || '0', 10); var $queueCell = $row.find('td').eq(3); @@ -501,87 +520,6 @@ $mergedGroups = [ } } - function mergeLiveRowsOrReload() { - $.getJSON(liveRowsUrl, function(resp) { - if (!resp || parseInt(resp.ok || 0, 10) !== 1 || !Array.isArray(resp.rows)) { - reloadAppointmentList(); - return; - } - - var $domRows = $('#opdAllTable tbody tr[data-opd-id]'); - if ($domRows.length !== resp.rows.length) { - reloadAppointmentList(); - return; - } - - var domMap = {}; - $domRows.each(function() { - var id = parseInt($(this).attr('data-opd-id') || '0', 10); - if (id > 0) { - domMap[id] = $(this); - } - }); - - var requiresFullReload = false; - - for (var i = 0; i < resp.rows.length; i++) { - var r = resp.rows[i] || {}; - var opdId = parseInt(r.opd_id || 0, 10); - if (!opdId || !domMap[opdId]) { - requiresFullReload = true; - break; - } - - var $row = domMap[opdId]; - var nextStatus = (r.status || 'booked').toString().toLowerCase(); - var prevStatus = (($row.attr('data-opd-status') || '').toLowerCase()); - - // Booked transitions usually change action layout significantly, keep a safe fallback. - if (nextStatus === 'booked' && prevStatus !== 'booked') { - requiresFullReload = true; - break; - } - - $row.attr('data-opd-status', nextStatus); - $row.attr('data-has-prescription', parseInt(r.has_prescription || 0, 10) === 1 ? '1' : '0'); - $row.attr('data-has-vitals', parseInt(r.has_vitals || 0, 10) === 1 ? '1' : '0'); - - applyStatusVisual($row, nextStatus); - updateQueueCell($row, r.queue_no || 0); - - if (nextStatus === 'waiting') { - ensureWaitingActionButtons($row, opdId); - } else if (nextStatus === 'visited') { - $row.find('.btn-opd-create-queue').remove(); - var $statusBtn = $row.find('.btn-opd-status').first(); - if ($statusBtn.length) { - $statusBtn.removeClass('btn-outline-success').addClass('btn-success').attr('data-opd-status', '2').text('Visit Done'); - } - } else if (nextStatus === 'cancelled') { - $row.find('.btn-opd-status').remove(); - $row.find('.btn-opd-create-queue').remove(); - } - - updateVitalsButtonState($row, r.has_prescription || 0, r.has_vitals || 0); - } - - if (requiresFullReload) { - reloadAppointmentList(); - return; - } - - if (opdDataTable) { - opdDataTable.rows().invalidate().draw(false); - } - - recalcStatusCounts(); - applyFilters(); - liveDigest = String(resp.digest || ''); - }).fail(function() { - // Keep screen usable even if live sync fails intermittently. - }); - } - function startLiveUpdates() { if (window.opdLiveMonitorTimer) { clearInterval(window.opdLiveMonitorTimer); @@ -920,12 +858,18 @@ $mergedGroups = [ }); }); + // Manual refresh button + $(document).off('click.opdrefresh', '#btn_manual_refresh').on('click.opdrefresh', '#btn_manual_refresh', function() { + clearRefreshSignal(); + reloadAppointmentList(); + }); + // Initialize registerOpdTableFilter(); opdDataTable = initializeOpdTable(); recalcStatusCounts(); syncWaitingHintAndBadge(); - startLiveUpdates(); + startLiveUpdates(); // lightweight digest poll — shows "Refresh" badge instead of auto-reloading })();