Add investigation shortcuts manager panel

This commit is contained in:
DevSoftTech Server 2026-04-14 21:51:49 +05:30
parent 57397d80e1
commit aae6211f48
4 changed files with 744 additions and 16 deletions

View File

@ -625,6 +625,7 @@ $routes->post('Opd_prescription/advice_remove/(:num)', 'Opd_prescription::advice
$routes->get('Opd_prescription/investigation_search', 'Opd_prescription::investigation_search');
$routes->get('Opd_prescription/investigation_shortcuts', 'Opd_prescription::investigation_shortcuts');
$routes->get('Opd_prescription/debug_investigation_fields', 'Opd_prescription::debug_investigation_fields');
$routes->get('Opd_prescription/investigation_list/(:num)', 'Opd_prescription::investigation_list/$1');
$routes->get('Opd_prescription/investigation_list/(:num)/(:num)', 'Opd_prescription::investigation_list/$1/$2');
$routes->post('Opd_prescription/investigation_add', 'Opd_prescription::investigation_add');
@ -636,6 +637,9 @@ $routes->get('Opd_prescription/opd_invest_master_get/(:num)', 'Opd_prescription:
$routes->post('Opd_prescription/opd_invest_master_save', 'Opd_prescription::opd_invest_master_save');
$routes->post('Opd_prescription/opd_invest_master_remove/(:num)', 'Opd_prescription::opd_invest_master_remove/$1');
$routes->post('Opd_prescription/opd_invest_master_toggle_fav/(:num)', 'Opd_prescription::opd_invest_master_toggle_fav/$1');
$routes->get('Opd_prescription/opd_invest_shortcuts_manager', 'Opd_prescription::opd_invest_shortcuts_manager', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']);
$routes->get('Opd_prescription/opd_invest_shortcuts_all', 'Opd_prescription::opd_invest_shortcuts_all');
$routes->post('Opd_prescription/opd_invest_shortcuts_save_item', 'Opd_prescription::opd_invest_shortcuts_save_item');
$routes->get('Opd_prescription/opd_invest_profile_master', 'Opd_prescription::opd_invest_profile_master', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']);
$routes->get('Opd_prescription/opd_invest_profile_master_list', 'Opd_prescription::opd_invest_profile_master_list');
$routes->get('Opd_prescription/opd_invest_profile_master_data', 'Opd_prescription::opd_invest_profile_master_data');

View File

@ -3381,6 +3381,85 @@ class Opd_prescription extends BaseController
]);
}
public function opd_invest_shortcuts_manager()
{
return view('billing/opd_invest_shortcuts_manager');
}
public function opd_invest_shortcuts_all()
{
if (! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
$codeField = $this->resolveFirstField($fields, ['Code', 'code']);
$nameField = $this->resolveFirstField($fields, ['Name', 'name']);
if ($keyField === null || $codeField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$selectCols = $keyField . ' as id, ' . $codeField . ' as code, ' . $nameField . ' as name';
foreach (['short_name', 'sort_id'] as $f) {
if (in_array($f, $fields, true)) {
$selectCols .= ', ' . $f;
}
}
$rows = $this->db->table('investigation')
->select($selectCols)
->orderBy($nameField, 'ASC')
->get()->getResultArray();
$rows = array_map(static function (array $r): array {
return [
'id' => (int) ($r['id'] ?? 0),
'code' => (string) ($r['code'] ?? ''),
'name' => (string) ($r['name'] ?? ''),
'short_name' => (string) ($r['short_name'] ?? ''),
'sort_id' => (int) ($r['sort_id'] ?? 0),
];
}, $rows);
return $this->response->setJSON([
'rows' => $rows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_shortcuts_save_item()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if (! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$id = (int) $this->request->getPost('id');
if ($id <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid id']);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
if ($keyField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']);
}
$data = [];
if (in_array('short_name', $fields, true)) {
$data['short_name'] = trim((string) $this->request->getPost('short_name'));
}
if (in_array('sort_id', $fields, true)) {
$data['sort_id'] = max(0, (int) $this->request->getPost('sort_id'));
}
if (empty($data)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Nothing to update']);
}
$this->db->table('investigation')->where($keyField, $id)->update($data);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Saved',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_profile_master()
{
return view('billing/opd_invest_profile_master_workspace');
@ -4697,25 +4776,36 @@ class Opd_prescription extends BaseController
if ($this->db->tableExists('investigation')) {
$invFields = $this->db->getFieldNames('investigation');
$codeField = $this->resolveFirstField($invFields, ['Code', 'code']);
$nameField = $this->resolveFirstField($invFields, ['Name', 'name']);
$shortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']);
$sortField = $this->resolveFirstField($invFields, ['sort_id', 'sort_order', 'id']);
if ($codeField !== null && $nameField !== null && $shortField !== null) {
$builder = $this->db->table('investigation')
->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name')
$pickField = static function (array $fields, array $candidates): ?string {
$map = [];
foreach ($fields as $field) {
$map[strtolower((string) $field)] = (string) $field;
}
foreach ($candidates as $candidate) {
$key = strtolower((string) $candidate);
if (isset($map[$key])) {
return $map[$key];
}
}
return null;
};
$codeField = $pickField($invFields, ['Code', 'code']);
$nameField = $pickField($invFields, ['Name', 'name']);
$shortField = $pickField($invFields, ['short_name', 'shortName', 'short']);
$sortField = $pickField($invFields, ['sort_id', 'sort_order', 'id']);
if ($codeField !== null && $nameField !== null && $shortField !== null && $sortField !== null) {
// Keep legacy CI3 behavior: show only mostly-used tests and group/order by short_name, sort_id.
$shortTests = $this->db->table('investigation')
->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name,' . $sortField . ' as sort_id')
->where($shortField . ' !=', '')
->where($shortField . ' is not null', null, false)
->limit(200);
if ($sortField !== null) {
$builder->orderBy($shortField, 'ASC')->orderBy($sortField, 'ASC');
} else {
$builder->orderBy($shortField, 'ASC')->orderBy($nameField, 'ASC');
}
$shortTests = $builder->get()->getResultArray();
->orderBy($shortField, 'ASC')
->orderBy($sortField, 'ASC')
->get()
->getResultArray();
}
}
@ -4796,6 +4886,36 @@ class Opd_prescription extends BaseController
]);
}
// DEBUG ENDPOINT - Remove after verifying data
public function debug_investigation_fields()
{
if ($this->db->tableExists('investigation')) {
$fields = $this->db->getFieldNames('investigation');
$total = $this->db->table('investigation')->countAll();
$withShortName = $this->db->table('investigation')
->where("short_name != ''", null, false)
->where('short_name IS NOT NULL', null, false)
->countAll();
$sample = $this->db->table('investigation')
->where("short_name != ''", null, false)
->where('short_name IS NOT NULL', null, false)
->orderBy('short_name', 'ASC')
->orderBy('sort_id', 'ASC')
->limit(10)
->get()
->getResultArray();
return $this->response->setJSON([
'table_fields' => $fields,
'total_investigations' => $total,
'with_short_name' => $withShortName,
'sample_data' => $sample,
]);
}
return $this->response->setJSON(['error' => 'investigation table not found']);
}
public function investigation_list($opdId, $sessionId = 0)
{
$table = $this->findExistingTable(['opd_prescription_investigation']);

View File

@ -0,0 +1,419 @@
<section class="container-fluid py-3">
<div class="d-flex align-items-center gap-2 mb-3">
<h5 class="mb-0"><i class="bi bi-grid-3x2 me-1"></i> Investigation Shortcuts Manager</h5>
<span class="badge bg-secondary" id="ism_badge">—</span>
<div class="ms-auto d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" id="btn_ism_new_group">
<i class="bi bi-plus-circle"></i> New Group
</button>
</div>
</div>
<!-- Live Preview -->
<div class="card mb-3 border-info">
<div class="card-header py-1 bg-info bg-opacity-10">
<small class="fw-semibold text-info-emphasis"><i class="bi bi-eye me-1"></i>Panel Preview (how it will look on OPD form)</small>
</div>
<div class="card-body py-2" id="ism_preview">
<span class="text-muted small">Loading…</span>
</div>
</div>
<div class="row g-3">
<!-- Groups (left) -->
<div class="col-md-7">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between py-2">
<strong>Groups in Panel</strong>
<span class="badge bg-primary" id="ism_in_panel_count">0</span>
</div>
<div class="card-body p-2" id="ism_groups_container" style="max-height:600px;overflow-y:auto;">
<div class="text-muted text-center py-3 small">Loading…</div>
</div>
</div>
</div>
<!-- Not in Panel (right) -->
<div class="col-md-5">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between py-2">
<strong>Not in Panel</strong>
<span class="badge bg-secondary" id="ism_not_in_panel_count">0</span>
</div>
<div class="card-header py-1 border-top-0">
<input type="text" id="ism_search_unassigned" class="form-control form-control-sm" placeholder="Search tests…">
</div>
<div class="card-body p-0" style="max-height:560px;overflow-y:auto;">
<div id="ism_unassigned_list">
<div class="text-muted text-center py-3 small">Loading…</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- New / Rename Group Modal -->
<div class="modal fade" id="ismGroupModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title" id="ismGroupModalTitle">New Group</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="ism_gmodal_old_name" value="">
<label class="form-label small">Group Name <span class="text-danger">*</span></label>
<input type="text" id="ism_gmodal_name" class="form-control form-control-sm" maxlength="80" placeholder="e.g. Blood Tests, Urine…">
<div class="small mt-1 text-danger" id="ism_gmodal_msg"></div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_ism_gmodal_ok">Create</button>
</div>
</div>
</div>
</div>
<!-- Assign Test to Group Modal -->
<div class="modal fade" id="ismAssignModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title">Assign to Group</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="ism_assign_id" value="0">
<p class="mb-2">Test: <strong id="ism_assign_name">—</strong></p>
<div class="mb-2">
<label class="form-label small">Group <span class="text-danger">*</span></label>
<select id="ism_assign_group_sel" class="form-select form-select-sm">
<option value="">— New group (type below) —</option>
</select>
</div>
<div class="mb-2" id="ism_assign_newgrp_wrap">
<input type="text" id="ism_assign_new_group" class="form-control form-control-sm" maxlength="80" placeholder="New group name…">
</div>
<div class="mb-2">
<label class="form-label small">Sort Order <small class="text-muted">(lower = first)</small></label>
<input type="number" id="ism_assign_sort" class="form-control form-control-sm" value="0" min="0">
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" id="btn_ism_assign_ok">Assign</button>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var allTests = [];
var csrfName = '<?= csrf_token() ?>';
var csrfHash = '<?= csrf_hash() ?>';
var COLOR_CLS = ['btn-secondary', 'btn-primary', 'btn-success', 'btn-info', 'btn-danger', 'btn-warning'];
function groupColor(idx) { return COLOR_CLS[idx % COLOR_CLS.length]; }
/* ── XHR helpers ────────────────────────────────────────── */
function apiGet(url, cb) {
$.getJSON(url).done(function (d) { cb(null, d); }).fail(function (x) { cb('HTTP ' + x.status, null); });
}
function apiPost(url, data, cb) {
data[csrfName] = csrfHash;
$.post(url, data, function (d) {
if (d && d.csrfHash) { csrfHash = d.csrfHash; if (d.csrfName) csrfName = d.csrfName; }
cb(d);
}).fail(function (x) { cb({ update: 0, error_text: 'HTTP ' + x.status }); });
}
/* ── Data helpers ───────────────────────────────────────── */
function computeGroups() {
var grp = {}, order = [];
allTests.forEach(function (t) {
var g = (t.short_name || '').trim();
if (!g) return;
if (!grp[g]) { grp[g] = []; order.push(g); }
grp[g].push(t);
});
order.sort();
order.forEach(function (g) {
grp[g].sort(function (a, b) { return (a.sort_id || 0) - (b.sort_id || 0); });
});
return { grp: grp, order: order };
}
function getUnassigned(filter) {
var f = (filter || '').toLowerCase();
return allTests.filter(function (t) {
if ((t.short_name || '').trim()) return false;
return !f || (t.name || '').toLowerCase().indexOf(f) !== -1 || (t.code || '').toLowerCase().indexOf(f) !== -1;
});
}
/* ── Render: preview ────────────────────────────────────── */
function renderPreview() {
var res = computeGroups();
if (!res.order.length) {
$('#ism_preview').html('<span class="text-muted small">No groups yet.</span>');
return;
}
var html = '';
res.order.forEach(function (g, ci) {
html += '<br>';
res.grp[g].forEach(function (t) {
html += '<button type="button" class="btn btn-sm ' + groupColor(ci) + '" style="margin:2px;" disabled>' + esc(t.name) + '</button>';
});
});
$('#ism_preview').html('<div>' + html + '</div>');
}
/* ── Render: groups ─────────────────────────────────────── */
function renderGroups() {
var res = computeGroups();
var inPanel = allTests.filter(function (t) { return (t.short_name || '').trim(); }).length;
$('#ism_in_panel_count').text(inPanel);
if (!res.order.length) {
$('#ism_groups_container').html(
'<div class="text-muted text-center py-4 small">' +
'No groups defined.<br>Click <strong>+ New Group</strong>, then assign tests from the right panel.' +
'</div>'
);
return;
}
var html = '';
res.order.forEach(function (g, ci) {
var bsCls = groupColor(ci);
html += '<div class="card mb-2">';
html += '<div class="card-header py-1 d-flex align-items-center gap-2">';
html += '<span class="badge ' + bsCls + '">' + esc(g) + '</span>';
html += '<small class="text-muted">(' + res.grp[g].length + ' tests)</small>';
html += '<div class="ms-auto d-flex gap-1">';
html += '<button class="btn btn-outline-secondary btn-sm py-0 px-2 btn-ism-rename" data-group="' + esc(g) + '">Rename</button>';
html += '<button class="btn btn-outline-danger btn-sm py-0 px-2 btn-ism-del-group" data-group="' + esc(g) + '">Remove Group</button>';
html += '</div></div>';
html += '<div class="card-body p-1">';
html += '<table class="table table-sm table-hover mb-0">';
html += '<thead class="table-light"><tr><th>Test Name</th><th width="85">Sort</th><th width="90">Action</th></tr></thead><tbody>';
res.grp[g].forEach(function (t) {
html += '<tr>';
html += '<td class="align-middle small">' + esc(t.name) + '</td>';
html += '<td><input type="number" class="form-control form-control-sm ism-sort-input" value="' + (t.sort_id || 0) + '" min="0" style="width:70px;" data-id="' + t.id + '"></td>';
html += '<td><button class="btn btn-outline-danger btn-sm py-0 btn-ism-remove" data-id="' + t.id + '">Remove</button></td>';
html += '</tr>';
});
html += '</tbody></table></div></div>';
});
$('#ism_groups_container').html(html);
}
/* ── Render: unassigned ─────────────────────────────────── */
function renderUnassigned() {
var filter = $('#ism_search_unassigned').val();
var list = getUnassigned(filter);
$('#ism_not_in_panel_count').text(allTests.filter(function (t) { return !(t.short_name || '').trim(); }).length);
if (!list.length) {
$('#ism_unassigned_list').html('<div class="text-muted text-center py-3 small">All tests are assigned to groups.</div>');
return;
}
var html = '<table class="table table-sm table-hover mb-0"><tbody>';
list.forEach(function (t) {
html += '<tr>';
html += '<td class="align-middle small">' + esc(t.name);
if (t.code) html += ' <span class="text-muted">(' + esc(t.code) + ')</span>';
html += '</td>';
html += '<td width="90" class="align-middle"><button class="btn btn-outline-primary btn-sm py-0 btn-ism-assign" data-id="' + t.id + '" data-name="' + esc(t.name) + '">+ Group</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
$('#ism_unassigned_list').html(html);
}
function renderAll() {
renderGroups();
renderUnassigned();
renderPreview();
syncGroupSelectOptions();
}
/* ── Sync group dropdown options ────────────────────────── */
function syncGroupSelectOptions() {
var res = computeGroups();
var $sel = $('#ism_assign_group_sel');
var cur = $sel.val();
$sel.empty().append('<option value="">— New group (type below) —</option>');
res.order.forEach(function (g) {
$sel.append('<option value="' + esc(g) + '"' + (g === cur ? ' selected' : '') + '>' + esc(g) + '</option>');
});
$sel.trigger('change');
}
/* ── Save one item ──────────────────────────────────────── */
var _saveTimers = {};
function saveItem(id, shortName, sortId, cb) {
apiPost('<?= base_url('Opd_prescription/opd_invest_shortcuts_save_item') ?>', {
id: id,
short_name: shortName,
sort_id: sortId
}, cb || function () {});
}
function debouncedSave(id, shortName, sortId) {
clearTimeout(_saveTimers[id]);
_saveTimers[id] = setTimeout(function () {
allTests.forEach(function (t) { if (t.id == id) { t.short_name = shortName; t.sort_id = sortId; } });
saveItem(id, shortName, sortId);
}, 700);
}
/* ── HTML escape ────────────────────────────────────────── */
function esc(s) { return $('<span>').text(s || '').html(); }
/* ── Event: sort input change ───────────────────────────── */
$(document).on('change', '.ism-sort-input', function () {
var id = parseInt($(this).data('id'), 10);
var val = Math.max(0, parseInt($(this).val(), 10) || 0);
var t = allTests.find(function (x) { return x.id === id; });
if (!t) return;
t.sort_id = val;
debouncedSave(id, t.short_name, val);
renderGroups(); // re-sort displayed list
renderPreview();
});
/* ── Event: remove test from panel ─────────────────────── */
$(document).on('click', '.btn-ism-remove', function () {
var id = parseInt($(this).data('id'), 10);
var t = allTests.find(function (x) { return x.id === id; });
if (!t) return;
saveItem(id, '', t.sort_id, function (d) {
if ((d.update || 0) !== 1) { alert(d.error_text || 'Failed'); return; }
t.short_name = '';
renderAll();
});
});
/* ── Event: rename group ────────────────────────────────── */
$(document).on('click', '.btn-ism-rename', function () {
var oldName = $(this).data('group');
$('#ism_gmodal_old_name').val(oldName);
$('#ism_gmodal_name').val(oldName);
$('#ism_gmodal_msg').text('');
$('#ismGroupModalTitle').text('Rename Group');
$('#btn_ism_gmodal_ok').text('Rename');
bootstrap.Modal.getOrCreateInstance('#ismGroupModal').show();
});
/* ── Event: delete group ────────────────────────────────── */
$(document).on('click', '.btn-ism-del-group', function () {
var g = $(this).data('group');
if (!confirm('Remove all tests from group "' + g + '"?\n\nTests will move to "Not in Panel".')) return;
var tasks = allTests.filter(function (t) { return (t.short_name || '').trim() === g; });
var done = 0;
if (!tasks.length) return;
tasks.forEach(function (t) {
saveItem(t.id, '', t.sort_id, function (d) {
if ((d.update || 0) === 1) t.short_name = '';
if (++done === tasks.length) renderAll();
});
});
});
/* ── New Group button ───────────────────────────────────── */
$('#btn_ism_new_group').on('click', function () {
$('#ism_gmodal_old_name').val('');
$('#ism_gmodal_name').val('');
$('#ism_gmodal_msg').text('');
$('#ismGroupModalTitle').text('New Group');
$('#btn_ism_gmodal_ok').text('Create');
bootstrap.Modal.getOrCreateInstance('#ismGroupModal').show();
});
/* ── Group modal OK ─────────────────────────────────────── */
$('#btn_ism_gmodal_ok').on('click', function () {
var newName = $('#ism_gmodal_name').val().trim();
var oldName = $('#ism_gmodal_old_name').val().trim();
if (!newName) { $('#ism_gmodal_msg').text('Group name is required.'); return; }
bootstrap.Modal.getOrCreateInstance('#ismGroupModal').hide();
if (oldName && oldName !== newName) {
// Rename: update all tests in oldName group
var tasks = allTests.filter(function (t) { return (t.short_name || '').trim() === oldName; });
if (!tasks.length) { renderAll(); return; }
var done = 0;
tasks.forEach(function (t) {
saveItem(t.id, newName, t.sort_id, function (d) {
if ((d.update || 0) === 1) t.short_name = newName;
if (++done === tasks.length) renderAll();
});
});
} else if (!oldName) {
// New group: add to dropdown so user can immediately assign tests to it
var res = computeGroups();
if (!res.grp[newName]) {
// Pre-populate dropdown with new name so assign modal shows it
$('#ism_assign_group_sel').append('<option value="' + esc(newName) + '" selected>' + esc(newName) + '</option>');
}
renderAll();
}
});
/* ── Assign test to group ───────────────────────────────── */
$(document).on('click', '.btn-ism-assign', function () {
var id = parseInt($(this).data('id'), 10);
var name = $(this).data('name');
$('#ism_assign_id').val(id);
$('#ism_assign_name').text(name);
$('#ism_assign_sort').val(0);
syncGroupSelectOptions();
bootstrap.Modal.getOrCreateInstance('#ismAssignModal').show();
});
$('#ism_assign_group_sel').on('change', function () {
var v = $(this).val();
if (v === '') {
$('#ism_assign_newgrp_wrap').show();
} else {
$('#ism_assign_newgrp_wrap').hide();
$('#ism_assign_new_group').val('');
}
});
$('#btn_ism_assign_ok').on('click', function () {
var id = parseInt($('#ism_assign_id').val(), 10);
var selGroup = $('#ism_assign_group_sel').val();
var newGroup = $('#ism_assign_new_group').val().trim();
var group = selGroup || newGroup;
var sortId = parseInt($('#ism_assign_sort').val(), 10) || 0;
if (!group) { alert('Please select an existing group or enter a new group name.'); return; }
bootstrap.Modal.getOrCreateInstance('#ismAssignModal').hide();
saveItem(id, group, sortId, function (d) {
if ((d.update || 0) !== 1) { alert(d.error_text || 'Failed to assign'); return; }
var t = allTests.find(function (x) { return x.id === id; });
if (t) { t.short_name = group; t.sort_id = sortId; }
renderAll();
});
});
/* ── Search unassigned ──────────────────────────────────── */
$('#ism_search_unassigned').on('input', function () { renderUnassigned(); });
/* ── Init ───────────────────────────────────────────────── */
apiGet('<?= base_url('Opd_prescription/opd_invest_shortcuts_all') ?>', function (err, d) {
allTests = (d && d.rows) ? d.rows : [];
$('#ism_badge').text(allTests.length + ' investigations');
renderAll();
});
})();
</script>

View File

@ -807,9 +807,17 @@
<option value="">Load profile...</option>
</select>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_apply_custom_inv_profile">Apply</button>
<button type="button" class="btn btn-outline-dark btn-sm" id="btn_toggle_legacy_inv_panel">Old Type Panel</button>
<a href="javascript:load_form_div('<?= base_url('Opd_prescription/opd_invest_shortcuts_manager') ?>','maindiv','Shortcuts Manager');" class="btn btn-outline-info btn-sm" title="Configure which tests appear in the Old Type Panel and their groups"><i class="bi bi-grid-3x2"></i> Manage Shortcuts</a>
<button type="button" class="btn btn-outline-dark btn-sm" id="btn_open_custom_investigation" data-bs-toggle="modal" data-bs-target="#customInvestigationModal">Custome Investigation</button>
</div>
</div>
<div id="legacy_inv_shortcuts_panel" class="border rounded p-2 mb-3" style="display:none;">
<div class="small fw-semibold mb-2">Legacy Investigation Shortcuts</div>
<div id="legacy_inv_shortcuts_grouped"></div>
</div>
<div class="row g-2 mb-3">
<div class="col-md-10">
<select class="form-select" id="investigation_name_select2" style="width:100%;">
@ -1230,6 +1238,26 @@
</div>
</div>
</div>
<div class="modal fade" id="customInvestigationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Custome Investigation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label class="form-label fw-semibold" for="custom_investigation_select2">Select Investigations</label>
<select class="form-select" id="custom_investigation_select2" multiple="multiple" style="width:100%;"></select>
<div class="small text-muted mt-2">Search and select multiple tests, then click Add Selected.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="btn_add_custom_investigations">Add Selected</button>
</div>
</div>
</div>
</div>
</section>
<script>
@ -1273,6 +1301,7 @@
var invCustomProfileKey = 'opd_inv_custom_profiles';
var legacyInvestigationProfiles = {};
var legacyInvestigationProfileList = [];
var legacyInvestigationShortTests = [];
var investigationBatchActive = false;
var activeMedicineScope = 'all';
var medicineSuggestRows = [];
@ -2785,6 +2814,7 @@
function loadInvestigationShortcutsFromLegacy() {
apiGet('<?= base_url('Opd_prescription/investigation_shortcuts') ?>', function(data) {
var profiles = (data && data.profiles) ? data.profiles : [];
var shortTests = (data && data.short_tests) ? data.short_tests : [];
var $profileWrap = $('#inv_profile_chip_wrap');
if ($profileWrap.length) {
@ -2792,6 +2822,8 @@
}
legacyInvestigationProfiles = {};
legacyInvestigationProfileList = [];
legacyInvestigationShortTests = Array.isArray(shortTests) ? shortTests : [];
renderLegacyInvestigationShortcuts(legacyInvestigationShortTests);
if (profiles.length) {
profiles.forEach(function(p) {
@ -2836,6 +2868,112 @@
});
}
function renderLegacyInvestigationShortcuts(rows) {
var $wrap = $('#legacy_inv_shortcuts_grouped');
if (!$wrap.length) {
return;
}
$wrap.empty();
rows = Array.isArray(rows) ? rows : [];
if (!rows.length) {
$wrap.append('<div class="text-muted small">No legacy investigations found.</div>');
return;
}
// Bootstrap 5 equivalents of CI3 Bootstrap 3 colors (btn-default removed in BS5 → btn-secondary)
var btnColor = ['btn-secondary', 'btn-primary', 'btn-success', 'btn-info', 'btn-danger', 'btn-warning'];
var shortName = '';
var counter = 1;
var btnStyle = 'btn-secondary';
var hasAny = false;
rows.forEach(function(row) {
var rowShortName = (row.short_name || '').toString().trim();
var testName = (row.name || '').toString().trim();
var testCode = (row.code || '').toString().trim();
// Skip if missing name or short_name
if (!testName || !rowShortName) {
return;
}
hasAny = true;
// CI3 LOGIC: if (row->short_name <> short_name) { echo '<br />'; pick new color; counter++ }
if (rowShortName !== shortName) {
$wrap.append('<br />');
btnStyle = btnColor[counter % btnColor.length];
counter++;
}
// btn-xs removed in Bootstrap 5 → use btn-sm
var $btn = $('<button type="button" class="btn btn-sm legacy-inv-chip inv-quick-chip" style="margin: 2px;"></button>')
.addClass(btnStyle)
.attr('data-test', testName)
.attr('data-code', testCode)
.text(testName);
$wrap.append($btn);
// Update short_name for next iteration
shortName = rowShortName;
});
if (!hasAny) {
$wrap.append('<div class="text-muted small">No legacy investigations found.</div>');
}
}
function initCustomInvestigationModal() {
var $modal = $('#customInvestigationModal');
var $sel = $('#custom_investigation_select2');
if (!$modal.length || !$sel.length || !($.fn && $.fn.select2)) {
return;
}
if ($sel.data('select2')) {
return;
}
$sel.select2({
width: '100%',
dropdownParent: $modal,
multiple: true,
placeholder: 'Search investigations...',
ajax: {
delay: 250,
transport: function(params, success, failure) {
var term = (params.data && params.data.term) ? params.data.term : '';
$.ajax({
url: '<?= base_url('Opd_prescription/investigation_search') ?>',
dataType: 'json',
data: { q: term },
success: success,
error: failure
});
},
processResults: function(data) {
var rows = (data && data.rows) ? data.rows : [];
return {
results: rows.map(function(row) {
var name = (row.name || '').toString();
var code = (row.code || '').toString();
return {
id: name + (code ? ' [' + code + ']' : ''),
text: name + (code ? ' [' + code + ']' : ''),
name: name,
code: code
};
})
};
}
}
});
$modal.on('hidden.bs.modal', function() {
$sel.val(null).trigger('change');
});
}
function initInvestigationPicker() {
var $select = $('#investigation_name_select2');
var $fallback = $('#investigation_name');
@ -5022,9 +5160,14 @@
$(document).on('click', '.inv-quick-chip', function() {
var test = ($(this).data('test') || '').toString().trim();
var code = ($(this).data('code') || '').toString().trim();
if (!test) {
return;
}
if (code) {
batchAddInvestigationRows([{ name: test, code: code }]);
return;
}
batchAddInvestigations([test]);
});
@ -5095,6 +5238,47 @@
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Invalid profile selection.');
});
$('#btn_toggle_legacy_inv_panel').on('click', function() {
var $panel = $('#legacy_inv_shortcuts_panel');
if (!$panel.length) {
return;
}
$panel.stop(true, true).slideToggle(140);
});
$('#btn_add_custom_investigations').on('click', function() {
var selected = [];
if ($.fn && $.fn.select2 && $('#custom_investigation_select2').length) {
selected = $('#custom_investigation_select2').select2('data') || [];
}
if (!selected.length) {
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Select at least one investigation.');
return;
}
var rows = selected.map(function(item) {
return {
name: (item.name || item.text || '').toString().trim(),
code: (item.code || '').toString().trim()
};
}).filter(function(item) {
return item.name !== '';
});
if (!rows.length) {
return;
}
batchAddInvestigationRows(rows);
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(document.getElementById('customInvestigationModal')).hide();
} else {
$('#customInvestigationModal').modal('hide');
}
});
$('#advise_investigation_notes').on('input', function() {
$('#investigation').val($(this).val() || '');
refreshCounters();
@ -5103,6 +5287,7 @@
refreshCustomProfileSelect();
loadInvestigationShortcutsFromLegacy();
initCustomInvestigationModal();
initInvestigationPicker();
loadInvSpecDropdown();