Add investigation shortcuts manager panel
This commit is contained in:
parent
57397d80e1
commit
aae6211f48
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
419
app/Views/billing/opd_invest_shortcuts_manager.php
Normal file
419
app/Views/billing/opd_invest_shortcuts_manager.php
Normal 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>
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user