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."
This commit is contained in:
parent
1987b4856b
commit
2958f2e9b6
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,9 +57,14 @@ $mergedGroups = [
|
|||
Cancelled <span id="countBadgeCancelled" class="badge bg-danger ms-1"><?= $countCancelled ?></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox" id="toggle_missing_vitals_only">
|
||||
<label class="form-check-label small" for="toggle_missing_vitals_only">Only Missing Vitals</label>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<button type="button" id="btn_manual_refresh" class="btn btn-sm btn-outline-secondary" title="Refresh list">
|
||||
↻ Refresh
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="toggle_missing_vitals_only">
|
||||
<label class="form-check-label small" for="toggle_missing_vitals_only">Only Missing Vitals</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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 <span class="badge bg-danger ms-1">New</span>');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user