diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php
index 199377d..d623ac8 100644
--- a/app/Controllers/Opd_prescription.php
+++ b/app/Controllers/Opd_prescription.php
@@ -390,8 +390,14 @@ class Opd_prescription extends BaseController
$fields = $this->db->getFieldNames('opd_prescription');
$patientRow = $this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->get(1)->getRowArray() ?? [];
+ $abhaField = $this->resolvePatientAbhaField();
+ $currentAbha = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : '';
+
$abhaAddress = trim((string) $this->request->getPost('abha_address'));
- if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) {
+ // Keep existing patient ABHA as-is even if it doesn't pass current runtime
+ // validator pattern; only validate when user enters/changes the value.
+ $abhaChanged = $abhaAddress !== '' && $abhaAddress !== $currentAbha;
+ if ($abhaChanged && ! $this->isValidAbhaAddress($abhaAddress)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid ABHA Address format.',
@@ -400,9 +406,7 @@ class Opd_prescription extends BaseController
]);
}
- $abhaField = $this->resolvePatientAbhaField();
if ($abhaField !== null && $abhaAddress !== '') {
- $currentAbha = trim((string) ($patientRow[$abhaField] ?? ''));
if ($currentAbha !== $abhaAddress) {
$this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->update([$abhaField => $abhaAddress]);
$this->auditClinicalUpdate('patient_master', $abhaField, (int) ($opdRow->p_id ?? 0), $currentAbha, $abhaAddress);
@@ -7235,6 +7239,7 @@ class Opd_prescription extends BaseController
$q = trim((string) $this->request->getGet('q'));
$favOnly = (int) $this->request->getGet('fav_only');
$specId = (int) $this->request->getGet('spec_id');
+ $category = strtolower(trim((string) $this->request->getGet('category')));
if (! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['rows' => []]);
@@ -7244,6 +7249,7 @@ class Opd_prescription extends BaseController
$codeField = in_array('Code', $fields, true) ? 'Code' : (in_array('code', $fields, true) ? 'code' : null);
$nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null);
$shortField = in_array('short_name', $fields, true) ? 'short_name' : null;
+ $categoryField = in_array('category_name', $fields, true) ? 'category_name' : null;
$hasFav = in_array('is_favourite', $fields, true);
$hasSpec = in_array('spec_ids', $fields, true);
@@ -7252,11 +7258,17 @@ class Opd_prescription extends BaseController
}
// When no search term but fav/spec filter applied, still return results
- if ($q === '' && $favOnly === 0 && $specId === 0) {
+ if ($q === '' && $favOnly === 0 && $specId === 0 && $category === '') {
return $this->response->setJSON(['rows' => []]);
}
$selectCols = $codeField . ' as code, ' . $nameField . ' as name';
+ if ($shortField !== null) {
+ $selectCols .= ', ' . $shortField . ' as short_name';
+ }
+ if ($categoryField !== null) {
+ $selectCols .= ', ' . $categoryField . ' as category_name';
+ }
if ($hasFav) {
$selectCols .= ', is_favourite';
}
@@ -7264,10 +7276,31 @@ class Opd_prescription extends BaseController
$builder = $this->db->table('investigation')->select($selectCols);
if ($q !== '') {
- $builder->groupStart()->like($nameField, $q);
- if ($shortField !== null) {
- $builder->orLike($shortField, $q);
+ $qLen = mb_strlen($q);
+ $builder->groupStart();
+
+ if ($qLen <= 2) {
+ // 2-char search: prefix only (AB*) for tighter result set.
+ $builder->like($nameField, $q, 'after');
+ if ($shortField !== null) {
+ $builder->orLike($shortField, $q, 'after');
+ }
+ } else {
+ // 3+ chars: prefix (ABC*) OR contains (*ABC*).
+ $builder->groupStart()
+ ->like($nameField, $q, 'after');
+ if ($shortField !== null) {
+ $builder->orLike($shortField, $q, 'after');
+ }
+ $builder->groupEnd()
+ ->orGroupStart()
+ ->like($nameField, $q, 'both');
+ if ($shortField !== null) {
+ $builder->orLike($shortField, $q, 'both');
+ }
+ $builder->groupEnd();
}
+
$builder->groupEnd();
}
@@ -7284,7 +7317,35 @@ class Opd_prescription extends BaseController
}
$builder->orderBy('LOWER(' . $this->db->protectIdentifiers($nameField) . ')', 'ASC', false);
- $rows = $builder->limit(40)->get()->getResultArray();
+ $rows = $builder->limit($category !== '' ? 200 : 40)->get()->getResultArray();
+
+ if ($category !== '') {
+ $normalizeCategory = static function (array $row): string {
+ $txt = strtolower(trim((string) (($row['category_name'] ?? '') . ' ' . ($row['short_name'] ?? '') . ' ' . ($row['name'] ?? ''))));
+ if ($txt === '') {
+ return 'pathology';
+ }
+ if (preg_match('/\bx\s*[-]?\s*ray\b|\bradiology\b|\bradiograph\b|\bct\b|\bmri\b/', $txt) === 1) {
+ return 'radiology';
+ }
+ if (preg_match('/\bultra\s*sound\b|\bultrasound\b|\busg\b|\bsonography\b/', $txt) === 1) {
+ return 'ultrasound';
+ }
+ if (preg_match('/\bcardi\b|\becg\b|\becho\b|\btmt\b/', $txt) === 1) {
+ return 'cardic';
+ }
+ return 'pathology';
+ };
+
+ $rows = array_values(array_filter($rows, static function (array $row) use ($normalizeCategory, $category): bool {
+ return $normalizeCategory($row) === $category;
+ }));
+
+ if (count($rows) > 40) {
+ $rows = array_slice($rows, 0, 40);
+ }
+ }
+
return $this->response->setJSON(['rows' => $rows]);
}
@@ -7341,6 +7402,9 @@ class Opd_prescription extends BaseController
$jId = $this->resolveFirstField($jFields, ['id']);
$iCode = $this->resolveFirstField($iFields, ['Code', 'code']);
$iName = $this->resolveFirstField($iFields, ['Name', 'name']);
+ $iShort = $this->resolveFirstField($iFields, ['short_name', 'shortName', 'short']);
+ $iCategory = $this->resolveFirstField($iFields, ['category_name', 'category']);
+ $iSpecIds = $this->resolveFirstField($iFields, ['spec_ids']);
if ($pCode !== null && $pName !== null && $jProfileCode !== null && $jInvestCode !== null && $iCode !== null && $iName !== null) {
$orderExpr = 'p.`' . $pName . '` ASC';
@@ -7356,7 +7420,19 @@ class Opd_prescription extends BaseController
. 'p.`' . $pCode . '` AS profile_code, '
. 'p.`' . $pName . '` AS profile_name, '
. 'i.`' . $iCode . '` AS investigation_code, '
- . 'i.`' . $iName . '` AS investigation_name '
+ . 'i.`' . $iName . '` AS investigation_name';
+
+ if ($iShort !== null) {
+ $sql .= ', i.`' . $iShort . '` AS short_name';
+ }
+ if ($iCategory !== null) {
+ $sql .= ', i.`' . $iCategory . '` AS category_name';
+ }
+ if ($iSpecIds !== null) {
+ $sql .= ', i.`' . $iSpecIds . '` AS spec_ids';
+ }
+
+ $sql .= ' '
. 'FROM `invprofiles` p '
. 'JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` '
. 'JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` '
@@ -7370,6 +7446,9 @@ class Opd_prescription extends BaseController
$profileName = trim((string) ($row['profile_name'] ?? ''));
$testCode = trim((string) ($row['investigation_code'] ?? ''));
$testName = trim((string) ($row['investigation_name'] ?? ''));
+ $shortName = trim((string) ($row['short_name'] ?? ''));
+ $categoryName = trim((string) ($row['category_name'] ?? ''));
+ $specIds = trim((string) ($row['spec_ids'] ?? ''));
if ($profileCode === '' || $profileName === '' || $testName === '') {
continue;
@@ -7393,6 +7472,9 @@ class Opd_prescription extends BaseController
$profileMap[$profileCode]['tests'][] = [
'code' => $testCode,
'name' => $testName,
+ 'short_name' => $shortName,
+ 'category_name' => $categoryName,
+ 'spec_ids' => $specIds,
];
}
diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php
index fd3f694..23315aa 100644
--- a/app/Views/billing/opd_prescription_basic.php
+++ b/app/Views/billing/opd_prescription_basic.php
@@ -359,6 +359,68 @@
border-color: #86b7fe !important;
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.12);
}
+ .rx-jump-bookmarks {
+ position: fixed;
+ right: 14px;
+ top: 52%;
+ transform: translateY(-50%);
+ z-index: 1100;
+ display: flex;
+ flex-direction: column;
+ gap: .35rem;
+ border: 1px solid #d7e3f4;
+ background: rgba(248, 251, 255, .96);
+ border-radius: .6rem;
+ padding: .45rem;
+ box-shadow: 0 8px 24px rgba(15, 23, 42, .12);
+ }
+ .rx-jump-bookmarks .rx-jump-drag-handle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ border: 1px dashed #b9cff4;
+ border-radius: .45rem;
+ background: #eef4ff;
+ color: #355f98;
+ font-size: .72rem;
+ padding: .12rem .35rem;
+ margin-bottom: .1rem;
+ cursor: grab;
+ user-select: none;
+ touch-action: none;
+ }
+ .rx-jump-bookmarks.is-dragging .rx-jump-drag-handle {
+ cursor: grabbing;
+ background: #dce9ff;
+ }
+ .rx-jump-bookmarks .rx-jump-link {
+ display: inline-flex;
+ align-items: center;
+ gap: .35rem;
+ border-radius: 999px;
+ padding: .2rem .6rem;
+ font-size: .8rem;
+ justify-content: flex-start;
+ white-space: nowrap;
+ }
+ .rx-jump-bookmarks .rx-jump-link i {
+ font-size: .82rem;
+ }
+ @media (max-width: 991.98px) {
+ .rx-jump-bookmarks {
+ right: 8px;
+ top: auto;
+ bottom: 10px;
+ transform: none;
+ max-height: 62vh;
+ overflow-y: auto;
+ }
+ .rx-jump-bookmarks .rx-jump-link {
+ padding: .2rem .45rem;
+ font-size: .75rem;
+ }
+ }
.rx-prefilled-morbidity {
background: #f8fbff;
border: 1px solid #d6e8ff;
@@ -424,13 +486,6 @@
No local changes
-
-
-
id ?? 0) . '/1?' . http_build_query([
@@ -457,9 +512,28 @@
aria-label="Patient History">
-
-
+
+
+
+
+
+
+
+
Move
+
+
+
+
+
+
+
@@ -490,10 +585,10 @@
UHID: = esc($patient_master[0]->p_code ?? '') ?>
Age: = esc($patient_master[0]->str_age ?? '') ?>
Gender: = esc($patient_master[0]->xgender ?? '') ?>
-
-
-
-
+
+
ABHA Address: = esc($patientAbhaAddress) ?>
+
+
OPD: = esc($opd_master[0]->opd_code ?? '') ?>
Date: = esc($opd_master[0]->apointment_date ?? '') ?>
Doctor: = esc($opd_master[0]->doc_name ?? '') ?>
@@ -606,19 +701,6 @@
-
-
-
+
@@ -1068,9 +1150,9 @@
+
+
-
Recent investigations
-
Loading...
@@ -1086,12 +1168,35 @@
+
+
Legacy Investigation Shortcuts
-
+
-
+
| Test Name | Code | Action |
| No investigation added |
@@ -1605,6 +1710,7 @@
};
var patientId = = (int) ($patient_master[0]->id ?? 0) ?>;
var doctorViewId = = (int) ($doctorId ?? 0) ?>;
+ var jumpBookmarkPositionKey = 'opd_jump_bookmark_position_' + doctorViewId;
autotypeSuggestCacheLocalKey = autotypeSuggestCacheLocalKey + '_' + doctorViewId;
var consultSectionPreferenceKey = 'opd_consult_section_pref_' + doctorViewId;
var consultSectionMeta = [
@@ -1755,6 +1861,129 @@
});
}
+ function clampJumpBookmarkPosition(left, top, panelWidth, panelHeight) {
+ var viewportWidth = Math.max($(window).width() || 0, 320);
+ var viewportHeight = Math.max($(window).height() || 0, 320);
+ var safeLeft = Math.min(Math.max(left, 6), Math.max(6, viewportWidth - panelWidth - 6));
+ var safeTop = Math.min(Math.max(top, 6), Math.max(6, viewportHeight - panelHeight - 6));
+ return { left: safeLeft, top: safeTop };
+ }
+
+ function saveJumpBookmarkPosition(pos) {
+ try {
+ localStorage.setItem(jumpBookmarkPositionKey, JSON.stringify(pos || {}));
+ } catch (e) {
+ // Ignore private mode/quota errors.
+ }
+ }
+
+ function applySavedJumpBookmarkPosition($panel) {
+ if (!$panel || !$panel.length) {
+ return;
+ }
+ try {
+ var raw = localStorage.getItem(jumpBookmarkPositionKey);
+ if (!raw) {
+ return;
+ }
+ var parsed = JSON.parse(raw);
+ var left = Number(parsed && parsed.left);
+ var top = Number(parsed && parsed.top);
+ if (!isFinite(left) || !isFinite(top)) {
+ return;
+ }
+ var panelWidth = $panel.outerWidth() || 180;
+ var panelHeight = $panel.outerHeight() || 220;
+ var pos = clampJumpBookmarkPosition(left, top, panelWidth, panelHeight);
+ $panel.css({ left: pos.left + 'px', top: pos.top + 'px', right: 'auto', bottom: 'auto', transform: 'none' });
+ } catch (e) {
+ // Ignore parse failures.
+ }
+ }
+
+ function initJumpBookmarkDrag() {
+ var $panel = $('.rx-jump-bookmarks').first();
+ if (!$panel.length) {
+ return;
+ }
+
+ applySavedJumpBookmarkPosition($panel);
+
+ var dragState = { active: false, pointerId: null, startX: 0, startY: 0, baseLeft: 0, baseTop: 0 };
+
+ function getPointerCoords(ev) {
+ if (ev && ev.originalEvent && ev.originalEvent.touches && ev.originalEvent.touches.length) {
+ return { x: ev.originalEvent.touches[0].clientX, y: ev.originalEvent.touches[0].clientY };
+ }
+ if (ev && ev.originalEvent && ev.originalEvent.changedTouches && ev.originalEvent.changedTouches.length) {
+ return { x: ev.originalEvent.changedTouches[0].clientX, y: ev.originalEvent.changedTouches[0].clientY };
+ }
+ return { x: ev.clientX, y: ev.clientY };
+ }
+
+ function beginDrag(ev) {
+ var coords = getPointerCoords(ev);
+ var panelOffset = $panel.offset() || { left: 14, top: 100 };
+ dragState.active = true;
+ dragState.pointerId = (ev.originalEvent && typeof ev.originalEvent.pointerId !== 'undefined') ? ev.originalEvent.pointerId : null;
+ dragState.startX = coords.x;
+ dragState.startY = coords.y;
+ dragState.baseLeft = panelOffset.left;
+ dragState.baseTop = panelOffset.top;
+ $panel.addClass('is-dragging').css({ left: panelOffset.left + 'px', top: panelOffset.top + 'px', right: 'auto', bottom: 'auto', transform: 'none' });
+ ev.preventDefault();
+ }
+
+ function moveDrag(ev) {
+ if (!dragState.active) {
+ return;
+ }
+ if (dragState.pointerId !== null && ev.originalEvent && typeof ev.originalEvent.pointerId !== 'undefined' && ev.originalEvent.pointerId !== dragState.pointerId) {
+ return;
+ }
+ var coords = getPointerCoords(ev);
+ var nextLeft = dragState.baseLeft + (coords.x - dragState.startX);
+ var nextTop = dragState.baseTop + (coords.y - dragState.startY);
+ var panelWidth = $panel.outerWidth() || 180;
+ var panelHeight = $panel.outerHeight() || 220;
+ var pos = clampJumpBookmarkPosition(nextLeft, nextTop, panelWidth, panelHeight);
+ $panel.css({ left: pos.left + 'px', top: pos.top + 'px', right: 'auto', bottom: 'auto', transform: 'none' });
+ ev.preventDefault();
+ }
+
+ function endDrag() {
+ if (!dragState.active) {
+ return;
+ }
+ dragState.active = false;
+ dragState.pointerId = null;
+ $panel.removeClass('is-dragging');
+ var panelOffset = $panel.offset() || { left: 14, top: 100 };
+ saveJumpBookmarkPosition({ left: panelOffset.left, top: panelOffset.top });
+ }
+
+ $panel.off('pointerdown.rxJumpDrag touchstart.rxJumpDrag mousedown.rxJumpDrag', '.rx-jump-drag-handle');
+ $panel.on('pointerdown.rxJumpDrag touchstart.rxJumpDrag mousedown.rxJumpDrag', '.rx-jump-drag-handle', beginDrag);
+
+ $(document).off('pointermove.rxJumpDrag touchmove.rxJumpDrag mousemove.rxJumpDrag');
+ $(document).on('pointermove.rxJumpDrag touchmove.rxJumpDrag mousemove.rxJumpDrag', moveDrag);
+
+ $(document).off('pointerup.rxJumpDrag pointercancel.rxJumpDrag touchend.rxJumpDrag touchcancel.rxJumpDrag mouseup.rxJumpDrag');
+ $(document).on('pointerup.rxJumpDrag pointercancel.rxJumpDrag touchend.rxJumpDrag touchcancel.rxJumpDrag mouseup.rxJumpDrag', endDrag);
+
+ $(window).off('resize.rxJumpDrag').on('resize.rxJumpDrag', function() {
+ var panelOffset = $panel.offset();
+ if (!panelOffset) {
+ return;
+ }
+ var panelWidth = $panel.outerWidth() || 180;
+ var panelHeight = $panel.outerHeight() || 220;
+ var pos = clampJumpBookmarkPosition(panelOffset.left, panelOffset.top, panelWidth, panelHeight);
+ $panel.css({ left: pos.left + 'px', top: pos.top + 'px', right: 'auto', bottom: 'auto', transform: 'none' });
+ saveJumpBookmarkPosition({ left: pos.left, top: pos.top });
+ });
+ }
+
function setAbdmStatus(msg, level) {
var $box = $('#abdm_action_status');
if (!$box.length) {
@@ -6661,6 +6890,332 @@
});
}
+ function normalizeInvestigationCategoryLabel(label) {
+ var txt = (label || '').toString().trim().toLowerCase();
+ if (!txt) {
+ return 'Pathology';
+ }
+ if (/\bx\s*[-]?\s*ray\b/i.test(txt) || txt.indexOf('xray') !== -1 || txt.indexOf('radiology') !== -1 || txt === 'x') {
+ return 'X-Ray';
+ }
+ if (txt.indexOf('ultra') !== -1 || txt.indexOf('ultra sound') !== -1 || txt.indexOf('ultrasound') !== -1 || txt.indexOf('usg') !== -1 || txt.indexOf('sonography') !== -1) {
+ return 'Ultra Sound';
+ }
+ if (txt.indexOf('card') !== -1 || txt.indexOf('cardio') !== -1 || txt.indexOf('ecg') !== -1 || txt.indexOf('echo') !== -1 || txt.indexOf('tmt') !== -1) {
+ return 'Cardic Test';
+ }
+ return 'Pathology';
+ }
+
+ function normalizeInvestigationTextKey(value) {
+ return (value || '')
+ .toString()
+ .toLowerCase()
+ .replace(/[()\[\]{}.,]/g, ' ')
+ .replace(/[-_/]+/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ function buildInvestigationCategoryMap() {
+ var map = {};
+ (legacyInvestigationShortTests || []).forEach(function(row) {
+ var name = (row && row.name ? row.name : '').toString().trim().toLowerCase();
+ var code = (row && row.code ? row.code : '').toString().trim().toLowerCase();
+ var category = normalizeInvestigationCategoryLabel(row && row.short_name ? row.short_name : '');
+ if (name) {
+ map['n:' + name] = category;
+ map['n:' + normalizeInvestigationTextKey(name)] = category;
+ }
+ if (code) {
+ map['c:' + code] = category;
+ }
+ });
+ return map;
+ }
+
+ function detectInvestigationCategory(row, categoryMap) {
+ var name = (row && row.name ? row.name : '').toString().trim();
+ var code = (row && row.code ? row.code : '').toString().trim();
+ var shortName = (row && row.short_name ? row.short_name : '').toString().trim();
+ var categoryName = (row && row.category_name ? row.category_name : '').toString().trim();
+ var nameNorm = normalizeInvestigationTextKey(name);
+
+ // Name-based category must win for explicit tests like X-Ray Chest,
+ // even when master short/category fields are generic.
+ if (/\bx\s*[-]?\s*ray\b|\bpa\s*view\b|\bradiograph\b/i.test(nameNorm)) {
+ return 'X-Ray';
+ }
+ if (/\bultra\s*sound\b|\bultrasound\b|\busg\b|\bsonography\b/i.test(nameNorm)) {
+ return 'Ultra Sound';
+ }
+ if (/\becg\b|\becho\b|\btmt\b|\bcardi/i.test(nameNorm)) {
+ return 'Cardic Test';
+ }
+
+ if (categoryName) {
+ return normalizeInvestigationCategoryLabel(categoryName);
+ }
+
+ if (shortName) {
+ return normalizeInvestigationCategoryLabel(shortName);
+ }
+
+ var byCodeKey = 'c:' + code.toLowerCase();
+ if (code && categoryMap[byCodeKey]) {
+ return categoryMap[byCodeKey];
+ }
+
+ var byNameKey = 'n:' + name.toLowerCase();
+ if (name && categoryMap[byNameKey]) {
+ return categoryMap[byNameKey];
+ }
+
+ var byNameNormKey = 'n:' + nameNorm;
+ if (nameNorm && categoryMap[byNameNormKey]) {
+ return categoryMap[byNameNormKey];
+ }
+
+ return normalizeInvestigationCategoryLabel(name);
+ }
+
+ function clearInvestigationCategorySelections() {
+ ['#inv_cat_pathology', '#inv_cat_radiology', '#inv_cat_ultrasound', '#inv_cat_cardic'].forEach(function(sel) {
+ var $sel = $(sel);
+ if (!$sel.length) {
+ return;
+ }
+ $sel.val(null).trigger('change.select2');
+ $sel.find('option').remove();
+ });
+ $('#inv_cat_others').val('');
+ }
+
+ function appendInvestigationProfileToNotes(rows) {
+ rows = Array.isArray(rows) ? rows : [];
+ if (!rows.length) {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('No tests found in selected profile.');
+ return;
+ }
+
+ clearInvestigationCategorySelections();
+
+ var categoryOrder = ['Pathology', 'X-Ray', 'Ultra Sound', 'Cardic Test'];
+ var grouped = {
+ 'Pathology': [],
+ 'X-Ray': [],
+ 'Ultra Sound': [],
+ 'Cardic Test': []
+ };
+ var seen = {};
+ var categoryMap = buildInvestigationCategoryMap();
+
+ rows.forEach(function(row) {
+ var name = (row && row.name ? row.name : row).toString().trim();
+ var code = (row && row.code ? row.code : '').toString().trim();
+ if (!name) {
+ return;
+ }
+ var uniq = (code + '|' + name).toLowerCase();
+ if (seen[uniq]) {
+ return;
+ }
+ seen[uniq] = true;
+
+ var category = detectInvestigationCategory({
+ name: name,
+ code: code,
+ short_name: (row && row.short_name ? row.short_name : ''),
+ category_name: (row && row.category_name ? row.category_name : '')
+ }, categoryMap);
+ if (!grouped[category]) {
+ grouped[category] = [];
+ categoryOrder.push(category);
+ }
+ grouped[category].push(name);
+ });
+
+ fillCategorySelectorsFromRows(rows, categoryMap, categoryOrder, grouped);
+ buildInvestigationNotesFromCategories();
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Investigation profile applied to category builder.');
+ markDirty('Investigation profile applied');
+ }
+
+ function ensureSelect2Option($sel, name, code) {
+ if (!$sel.length || !name) {
+ return;
+ }
+ var optionId = (name + ' [' + (code || '') + ']').trim();
+ var label = code ? (name + ' [' + code + ']') : name;
+ var exists = $sel.find('option').filter(function() {
+ return ($(this).val() || '') === optionId;
+ }).length > 0;
+ if (!exists) {
+ var opt = new Option(label, optionId, true, true);
+ $(opt).attr('data-name', name).attr('data-code', code || '');
+ $sel.append(opt);
+ }
+ }
+
+ function getSelectedCategoryNames(selector) {
+ var $sel = $(selector);
+ if (!$sel.length) {
+ return [];
+ }
+ var out = [];
+ var seen = {};
+ $sel.find('option:selected').each(function() {
+ var name = ($(this).attr('data-name') || $(this).text() || '').toString().trim();
+ name = name.replace(/\s*\[[^\]]*\]\s*$/, '').trim();
+ if (!name) {
+ return;
+ }
+ var key = name.toLowerCase();
+ if (seen[key]) {
+ return;
+ }
+ seen[key] = true;
+ out.push(name);
+ });
+ return out;
+ }
+
+ function buildInvestigationNotesFromCategories() {
+ var pathology = getSelectedCategoryNames('#inv_cat_pathology');
+ var radiology = getSelectedCategoryNames('#inv_cat_radiology');
+ var ultrasound = getSelectedCategoryNames('#inv_cat_ultrasound');
+ var cardic = getSelectedCategoryNames('#inv_cat_cardic');
+ var othersRaw = ($('#inv_cat_others').val() || '').toString().trim();
+
+ var lines = [];
+ if (pathology.length) {
+ lines.push('Pathology : ' + pathology.join(', '));
+ }
+ if (radiology.length) {
+ lines.push('X-Ray : ' + radiology.join(', '));
+ }
+ if (ultrasound.length) {
+ lines.push('Ultra Sound : ' + ultrasound.join(', '));
+ }
+ if (cardic.length) {
+ lines.push('Cardic Test : ' + cardic.join(', '));
+ }
+ if (othersRaw !== '') {
+ lines.push('Others : ' + othersRaw);
+ }
+
+ $('#advise_investigation_notes').val(lines.join('\n').trim()).trigger('input');
+ }
+
+ function fillCategorySelectorsFromRows(rows, categoryMap) {
+ var idMap = {
+ 'Pathology': '#inv_cat_pathology',
+ 'X-Ray': '#inv_cat_radiology',
+ 'Ultra Sound': '#inv_cat_ultrasound',
+ 'Cardic Test': '#inv_cat_cardic'
+ };
+
+ rows.forEach(function(row) {
+ var name = (row && row.name ? row.name : row).toString().trim();
+ var code = (row && row.code ? row.code : '').toString().trim();
+ if (!name) {
+ return;
+ }
+
+ var category = detectInvestigationCategory({
+ name: name,
+ code: code,
+ short_name: (row && row.short_name ? row.short_name : ''),
+ category_name: (row && row.category_name ? row.category_name : '')
+ }, categoryMap || buildInvestigationCategoryMap());
+
+ var target = idMap[category] || '#inv_cat_pathology';
+ ensureSelect2Option($(target), name, code);
+ });
+
+ Object.keys(idMap).forEach(function(k) {
+ var sel = idMap[k];
+ if ($(sel).length) {
+ $(sel).trigger('change.select2');
+ }
+ });
+ }
+
+ function parseInvestigationCategoryLabel(rawLabel) {
+ var label = (rawLabel || '').toString().trim().toLowerCase();
+ if (!label) {
+ return '';
+ }
+ if (label.indexOf('pathology') === 0) {
+ return 'pathology';
+ }
+ if (label.indexOf('x-ray') === 0 || label.indexOf('radiology') === 0 || label.indexOf('xray') === 0) {
+ return 'radiology';
+ }
+ if (label.indexOf('ultra sound') === 0 || label.indexOf('ultrasound') === 0 || label.indexOf('usg') === 0) {
+ return 'ultrasound';
+ }
+ if (label.indexOf('cardic') === 0 || label.indexOf('cardiac') === 0 || label.indexOf('cardio') === 0) {
+ return 'cardic';
+ }
+ if (label.indexOf('other') === 0) {
+ return 'others';
+ }
+ return '';
+ }
+
+ function hydrateCategoryBuilderFromNotes(textRaw) {
+ var text = (textRaw || '').toString();
+ if (text.trim() === '') {
+ return;
+ }
+
+ clearInvestigationCategorySelections();
+
+ var mapSel = {
+ pathology: '#inv_cat_pathology',
+ radiology: '#inv_cat_radiology',
+ ultrasound: '#inv_cat_ultrasound',
+ cardic: '#inv_cat_cardic'
+ };
+
+ var lines = text.split(/\r?\n/);
+ lines.forEach(function(line) {
+ var m = /^\s*([^:]+)\s*:\s*(.*)$/.exec(line || '');
+ if (!m) {
+ return;
+ }
+
+ var key = parseInvestigationCategoryLabel(m[1] || '');
+ var body = (m[2] || '').toString().trim();
+
+ if (key === 'others') {
+ $('#inv_cat_others').val(body);
+ return;
+ }
+
+ var target = mapSel[key] || '';
+ if (!target || body === '') {
+ return;
+ }
+
+ body.split(',').map(function(v) {
+ return (v || '').toString().trim();
+ }).filter(function(v) {
+ return v !== '';
+ }).forEach(function(name) {
+ ensureSelect2Option($(target), name, '');
+ });
+ });
+
+ Object.keys(mapSel).forEach(function(k) {
+ var sel = mapSel[k];
+ if ($(sel).length) {
+ $(sel).trigger('change.select2');
+ }
+ });
+ }
+
function loadInvestigationList() {
var opdId = $('#opd_id').val();
var sid = parseInt($('#opd_session_id').val() || '0', 10);
@@ -6791,7 +7346,7 @@
$(document).off('click.invProfileChip', '.inv-profile-chip').on('click.invProfileChip', '.inv-profile-chip', function() {
var key = ($(this).data('profile') || '').toString().trim();
if (legacyInvestigationProfiles[key] && legacyInvestigationProfiles[key].length) {
- batchAddInvestigationRows(legacyInvestigationProfiles[key]);
+ appendInvestigationProfileToNotes(legacyInvestigationProfiles[key]);
return;
}
@@ -6800,7 +7355,9 @@
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('No tests found in selected profile.');
return;
}
- batchAddInvestigations(tests);
+ appendInvestigationProfileToNotes(tests.map(function(name) {
+ return { name: (name || '').toString().trim(), code: '' };
+ }));
});
$('#btn_create_inv_profile').on('click', function() {
@@ -6848,13 +7405,93 @@
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Selected profile has no tests.');
return;
}
- batchAddInvestigationRows(legacyRows);
+ appendInvestigationProfileToNotes(legacyRows);
return;
}
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Invalid profile selection.');
});
+ function initInvestigationCategoryBuilders() {
+ if (!($.fn && $.fn.select2)) {
+ return;
+ }
+
+ $('.inv-cat-select').each(function() {
+ var $sel = $(this);
+ var category = ($sel.data('category') || '').toString();
+ $sel.select2({
+ width: '100%',
+ multiple: true,
+ placeholder: 'Search and select...',
+ minimumInputLength: 2,
+ allowClear: true,
+ language: {
+ inputTooShort: function() {
+ return 'Type at least 2 characters';
+ }
+ },
+ ajax: {
+ delay: 250,
+ transport: function(params, success, failure) {
+ var term = (params.data && params.data.term) ? params.data.term : '';
+ if ((term || '').trim().length < 2) {
+ success({ rows: [] });
+ return;
+ }
+ $.ajax({
+ url: '= base_url('Opd_prescription/investigation_search') ?>',
+ dataType: 'json',
+ data: { q: term, category: category },
+ 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();
+ var text = code ? (name + ' [' + code + ']') : name;
+ return { id: text, text: text, name: name, code: code };
+ })
+ };
+ }
+ },
+ templateSelection: function(item) {
+ return item.text || item.id || '';
+ }
+ }).on('select2:select', function(e) {
+ var d = (e && e.params && e.params.data) ? e.params.data : null;
+ if (!d) {
+ return;
+ }
+ var name = (d.name || d.text || '').toString().replace(/\s*\[[^\]]*\]\s*$/, '').trim();
+ var code = (d.code || '').toString().trim();
+ var val = (d.id || d.text || '').toString();
+ $sel.find('option').each(function() {
+ if (($(this).val() || '') === val) {
+ $(this).attr('data-name', name).attr('data-code', code);
+ }
+ });
+ buildInvestigationNotesFromCategories();
+ }).on('change', function() {
+ buildInvestigationNotesFromCategories();
+ });
+ });
+
+ $('#btn_build_inv_notes').on('click', function() {
+ buildInvestigationNotesFromCategories();
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Investigation advice text updated from category builder.');
+ markDirty('Investigation advice rebuilt');
+ });
+
+ $('#inv_cat_others').on('input', function() {
+ buildInvestigationNotesFromCategories();
+ });
+ }
+
$('#btn_toggle_legacy_inv_panel').on('click', function() {
var $panel = $('#legacy_inv_shortcuts_panel');
if (!$panel.length) {
@@ -6906,6 +7543,7 @@
loadInvestigationShortcutsFromLegacy();
initCustomInvestigationModal();
initInvestigationPicker();
+ initInvestigationCategoryBuilders();
loadInvSpecDropdown();
$('#btn_inv_fav_toggle').on('click', function() {
@@ -6926,7 +7564,9 @@
});
loadMedicineDoseMasters();
- $('#advise_investigation_notes').val($('#investigation').val() || '');
+ var savedInvestigationNotes = ($('#investigation').val() || '').toString();
+ $('#advise_investigation_notes').val(savedInvestigationNotes);
+ hydrateCategoryBuilderFromNotes(savedInvestigationNotes);
$(document).off('click.investRemoveSingle', '.btn-del-invest').on('click.investRemoveSingle', '.btn-del-invest', function() {
var id = $(this).data('id');
@@ -7949,6 +8589,45 @@
}
});
+ $(document).on('click', '.rx-jump-link', function() {
+ var targetSelector = ($(this).data('target') || '').toString();
+ if (targetSelector === '') {
+ return;
+ }
+
+ var $target = $(targetSelector);
+ if (!$target.length) {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Jump target not found on this consult page.');
+ return;
+ }
+
+ var $foldWrap = $target.closest('.rx-foldable');
+ if ($foldWrap.length && $foldWrap.hasClass('is-collapsed')) {
+ $foldWrap.removeClass('is-collapsed');
+ }
+
+ var targetEl = $target.get(0);
+ if (targetEl && typeof targetEl.scrollIntoView === 'function') {
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
+ }
+
+ setTimeout(function() {
+ var $scrollWrap = $target.closest('.rx-right-panel');
+ if ($scrollWrap.length) {
+ $scrollWrap.stop().animate({ scrollTop: Math.max(0, ($scrollWrap.scrollTop() || 0) - 18) }, 120);
+ } else {
+ var stickyHeight = $('.rx-sticky-actions:visible').outerHeight() || 0;
+ var jumpTop = ($target.offset() ? $target.offset().top : 0) - stickyHeight - 10;
+ $('html, body').stop().animate({ scrollTop: Math.max(0, jumpTop) }, 120);
+ }
+ }, 180);
+
+ $target.addClass('rx-scan-highlight');
+ setTimeout(function() {
+ $target.removeClass('rx-scan-highlight');
+ }, 1300);
+ });
+
$(document).on('click', '.btn-substitute-add', function() {
var medId = parseInt($(this).data('id') || '0', 10);
var medName = ($(this).data('name') || '').toString().trim();
@@ -8248,6 +8927,7 @@
});
initConsultSectionDesigner();
+ initJumpBookmarkDrag();
bindLegacyCommaAutocomplete('#complaints', '= base_url('Opd_prescription/complaints_search') ?>', function(row) {
return (row && row.name) ? row.name : '';