From 7f4b49a1c6015c827681af4937aec10cea7df29f Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Wed, 23 Sep 2026 07:38:28 +0530 Subject: [PATCH] OPD: add smart Form/Type-aware autocomplete for Dose and Strength --- app/Views/billing/opd_prescription_basic.php | 475 ++++++++++++++++++- 1 file changed, 472 insertions(+), 3 deletions(-) diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index bb1b6ca..d3415ab 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -1376,9 +1376,11 @@ -
+
- + + +
@@ -2414,7 +2416,7 @@ where: (data && data.where) ? data.where : [] }; - renderMedicineMasterSelectOptions($('#med_dosage'), medicineDoseMasterCache.dose, 'Dose'); + renderMedicineMasterSelectOptions($('#med_dosage_master'), medicineDoseMasterCache.dose, 'Dose'); renderMedicineMasterSelectOptions($('#med_when'), medicineDoseMasterCache.when, 'When'); renderMedicineMasterSelectOptions($('#med_freq_master'), medicineDoseMasterCache.freq, 'Freq'); renderMedicineMasterSelectOptions($('#med_where'), medicineDoseMasterCache.where, 'Route'); @@ -6382,6 +6384,469 @@ renderDiagnosisTable(); })(); + // ─── Dose / Strength Smart Autocomplete ────────────────────────────── + var medDosageHighlightIdx = -1; + + function getDosageCategory(formType, medName) { + var ft = (formType || '').toString().trim().toUpperCase(); + var mn = (medName || '').toString().trim().toUpperCase(); + if (!ft) { + if (/\b(TAB|TABLET|TABLETS)\b/i.test(mn)) ft = 'TAB'; + else if (/\b(CAP|CAPSULE|CAPSULES)\b/i.test(mn)) ft = 'CAP'; + else if (/\b(SYP|SYRUP|SUSP|SUSPENSION|SOLN|LIQUID)\b/i.test(mn)) ft = 'SYR'; + else if (/\b(INJ|INJECTION|INFUSION)\b/i.test(mn)) ft = 'INJ'; + else if (/\b(DROP|DROPS|EYE DROP|EAR DROP|NASAL DROP)\b/i.test(mn)) ft = 'DROPS'; + else if (/\b(OINT|OINTMENT|GEL|CREAM|LOTION)\b/i.test(mn)) ft = 'CREAM'; + else if (/\b(INHALER|PUFF|MDI|ROTACAP)\b/i.test(mn)) ft = 'INHALER'; + else if (/\b(RESPULE|RESPULES)\b/i.test(mn)) ft = 'RESPULES'; + else if (/\b(SACHET|GRANULES|POWDER)\b/i.test(mn)) ft = 'SACHET'; + } + + if (ft === 'TAB' || ft === 'TABLET' || ft === 'LOZENGE') return 'tablet'; + if (ft === 'CAP' || ft === 'CAPSULE') return 'capsule'; + if (ft === 'SYR' || ft === 'SUSP' || ft === 'SOLN' || ft === 'SYRUP' || ft === 'SUSPENSION') return 'syrup'; + if (ft.indexOf('DROP') !== -1 || ft === 'DROPS') return 'drops'; + if (ft === 'INHALER') return 'inhaler'; + if (ft === 'RESPULES') return 'respules'; + if (ft.indexOf('SPRAY') !== -1) return 'spray'; + if (ft === 'SACHET' || ft === 'POWDER') return 'sachet'; + if (ft === 'INJ' || ft === 'INFUSION' || ft === 'INJECTION') return 'injection'; + if (['CREAM', 'OINT', 'GEL', 'LOTION', 'SHAMPOO', 'SOAP', 'OIL', 'DUSTING POWDER', 'PAINT', 'PATCH'].indexOf(ft) !== -1) return 'topical'; + if (ft === 'SUPPOSITORY' || ft === 'ENEMA' || ft === 'PESSARY') return 'suppository'; + if (ft === 'GARGLE') return 'gargle'; + return 'general'; + } + + var _DOSAGE_PRESETS_BY_CAT = { + tablet: ['1 Tab', '1/2 Tab', '2 Tab', '1/4 Tab', '1.5 Tab', '500 mg', '650 mg', '250 mg', '100 mg', '50 mg', '10 mg', '5 mg', '20 mg', '40 mg'], + capsule: ['1 Cap', '2 Cap', '500 mg', '250 mg', '100 mg', '20 mg', '40 mg', '150 mg', '300 mg'], + syrup: ['5 ml (1 tsp)', '10 ml (2 tsp)', '2.5 ml (1/2 tsp)', '15 ml (1 tbsp)', '2 ml', '1 ml', '3 ml', '7.5 ml', '1 tsp', '2 tsp', '1 tbsp', '20 ml'], + drops: ['1 Drop', '2 Drops', '3 Drops', '4 Drops', '1-2 Drops', '2-3 Drops', '0.5 ml', '1 ml'], + inhaler: ['1 Puff', '2 Puffs', '1-2 Puffs', '3 Puffs'], + respules: ['1 Respule', '1/2 Respule', '2 Respules', '1 Respule in 2.5 ml Saline'], + spray: ['1 Spray each nostril', '2 Sprays each nostril', '1 Spray', '2 Sprays'], + sachet: ['1 Sachet in half glass water', '1 Sachet', '1/2 Sachet', '1 Scoop', '5 g', '10 g', '1 tsp'], + injection: ['1 Amp', '1 Vial', '1 ml', '2 ml', '5 ml', '10 ml', '100 ml', '500 ml', '100 mg', '250 mg', '500 mg', '1 g', '1000 IU', '5000 IU'], + topical: ['Apply Thinly', '1 Application', '1 Fingertip Unit (FTU)', 'Apply Generously', '1 Patch', 'Few Drops'], + suppository: ['1 Suppository', '1 Pessary', '1 Enema'], + gargle: ['10 ml with warm water', '15 ml with equal parts water', '5 ml', '10 ml'], + general: ['1 Tab', '1/2 Tab', '2 Tab', '1 Cap', '5 ml (1 tsp)', '10 ml (2 tsp)', '2.5 ml (1/2 tsp)', '500 mg', '650 mg', '250 mg', '1 Puff', '1 Sachet', '1 Drop', '1 Amp'] + }; + + function highlightMedDosageItem(idx) { + var $items = $('#med_dosage_dd .med-dosage-dd-item'); + if (!$items.length) return; + if (idx < 0) idx = 0; + if (idx >= $items.length) idx = $items.length - 1; + medDosageHighlightIdx = idx; + + $items.css('background', '').removeClass('active-dd-item'); + var $target = $items.eq(medDosageHighlightIdx); + $target.css('background', '#e2ebff').addClass('active-dd-item'); + + var container = document.getElementById('med_dosage_dd'); + var elem = $target[0]; + if (container && elem) { + var cTop = container.scrollTop; + var cBottom = cTop + container.clientHeight; + var eTop = elem.offsetTop; + var eBottom = eTop + elem.offsetHeight; + if (eTop < cTop) { + container.scrollTop = eTop; + } else if (eBottom > cBottom) { + container.scrollTop = eBottom - container.clientHeight; + } + } + } + + function getMedDosageSuggestions(input) { + var raw = (input || '').toString().trim(); + var q = raw.toLowerCase(); + var formType = ($('#med_type').val() || '').toString(); + var medName = ($('#med_name').val() || '').toString(); + var cat = getDosageCategory(formType, medName); + + var suggestions = [], seen = {}; + function add(val) { + if (!val) return; + var s = val.toString().trim(); + var k = s.toLowerCase(); + if (s && !seen[k]) { + seen[k] = true; + suggestions.push(s); + } + } + + var isHalf = (q === '1/2' || q === '0.5' || q === 'half' || q === '.5'); + var isQuarter = (q === '1/4' || q === '0.25' || q === '.25'); + var isOneAndHalf = (q === '1.5' || q === '1 1/2'); + var isTwoAndHalf = (q === '2.5' || q === '2 1/2'); + + if (isHalf) { + if (cat === 'tablet') { add('1/2 Tab'); add('1/4 Tab'); add('1 Tab'); } + else if (cat === 'capsule') { add('1/2 Cap'); add('1 Cap'); } + else if (cat === 'syrup') { add('2.5 ml (1/2 tsp)'); add('1/2 tsp (2.5 ml)'); add('2.5 ml'); add('5 ml (1 tsp)'); } + else if (cat === 'sachet') { add('1/2 Sachet'); add('1 Sachet'); } + else if (cat === 'respules') { add('1/2 Respule'); add('1 Respule'); } + else if (cat === 'drops') { add('0.5 ml'); add('1 Drop'); } + else { add('1/2 Tab'); add('2.5 ml (1/2 tsp)'); add('1/2 Sachet'); add('1/2 Cap'); } + } else if (isQuarter) { + if (cat === 'tablet') { add('1/4 Tab'); add('1/2 Tab'); } + else if (cat === 'syrup') { add('1.25 ml (1/4 tsp)'); } + else { add('1/4 Tab'); } + } else if (isOneAndHalf) { + if (cat === 'tablet') { add('1.5 Tab'); } + else if (cat === 'syrup') { add('7.5 ml (1.5 tsp)'); } + else { add('1.5 Tab'); add('7.5 ml'); } + } else if (isTwoAndHalf) { + if (cat === 'tablet') { add('2.5 Tab'); } + else if (cat === 'syrup') { add('2.5 ml (1/2 tsp)'); add('12.5 ml'); } + else { add('2.5 ml'); add('2.5 Tab'); } + } + + var numMatch = q.match(/^(\d+(?:\.\d+)?)/); + if (numMatch) { + var n = parseFloat(numMatch[1]); + var nStr = numMatch[1]; + + if (cat === 'tablet') { + if (n === 1) { + add('1 Tab'); + add('1/2 Tab'); + add('1/4 Tab'); + add('1.5 Tab'); + add('10 mg'); + add('100 mg'); + add('1000 mg (1 g)'); + } else if (n === 2) { + add('2 Tab'); + add('2.5 Tab'); + add('20 mg'); + add('200 mg'); + add('250 mg'); + } else if (n === 3) { + add('3 Tab'); + add('30 mg'); + add('300 mg'); + } else if (n === 4) { + add('4 Tab'); + add('1/4 Tab'); + add('40 mg'); + add('400 mg'); + } else if (n === 5) { + add('500 mg'); + add('50 mg'); + add('5 mg'); + add('1/2 Tab'); + add('5 Tab'); + } else if (n === 6) { + add('650 mg'); + add('60 mg'); + add('6 Tab'); + } else { + if (n < 10) add(nStr + ' Tab'); + add(nStr + ' mg'); + add(nStr + ' mcg'); + add(nStr + ' g'); + } + } else if (cat === 'capsule') { + if (n === 1) { + add('1 Cap'); + add('2 Cap'); + add('100 mg'); + add('150 mg'); + add('10 mg'); + } else if (n === 2) { + add('2 Cap'); + add('20 mg'); + add('200 mg'); + add('250 mg'); + } else if (n === 5) { + add('500 mg'); + add('50 mg'); + add('5 Cap'); + } else { + if (n < 10) add(nStr + ' Cap'); + add(nStr + ' mg'); + add(nStr + ' mcg'); + } + } else if (cat === 'syrup') { + if (n === 1) { + add('1 tsp (5 ml)'); + add('1 tbsp (15 ml)'); + add('1/2 tsp (2.5 ml)'); + add('1 ml'); + add('10 ml (2 tsp)'); + add('15 ml (1 tbsp)'); + } else if (n === 2) { + add('2 ml'); + add('2.5 ml (1/2 tsp)'); + add('2 tsp (10 ml)'); + add('20 ml'); + } else if (n === 3) { + add('3 ml'); + add('3.5 ml'); + add('30 ml'); + } else if (n === 4) { + add('4 ml'); + add('4.5 ml'); + } else if (n === 5) { + add('5 ml (1 tsp)'); + add('5 ml'); + add('2.5 ml (1/2 tsp)'); + add('15 ml (1 tbsp)'); + add('50 ml'); + } else if (n === 7 || n === 7.5) { + add('7.5 ml (1.5 tsp)'); + add('7 ml'); + } else if (n === 10) { + add('10 ml (2 tsp)'); + add('10 ml'); + } else if (n === 15) { + add('15 ml (1 tbsp)'); + add('15 ml'); + } else if (n === 20) { + add('20 ml'); + } else { + add(nStr + ' ml'); + if (n <= 4) add(nStr + ' tsp (' + (n * 5) + ' ml)'); + if (n <= 2) add(nStr + ' tbsp (' + (n * 15) + ' ml)'); + } + } else if (cat === 'drops') { + if (n === 1) { + add('1 Drop'); + add('1-2 Drops'); + add('0.5 ml'); + add('1 ml'); + } else if (n === 2) { + add('2 Drops'); + add('2-3 Drops'); + add('2 ml'); + } else if (n === 3) { + add('3 Drops'); + add('3-4 Drops'); + } else if (n === 4) { + add('4 Drops'); + add('4-5 Drops'); + } else if (n === 5) { + add('5 Drops'); + } else { + add(n === 1 ? '1 Drop' : (nStr + ' Drops')); + add(nStr + ' ml'); + } + } else if (cat === 'inhaler') { + if (n === 1) { + add('1 Puff'); + add('2 Puffs'); + add('1-2 Puffs'); + } else if (n === 2) { + add('2 Puffs'); + add('1-2 Puffs'); + } else { + add(n === 1 ? '1 Puff' : (nStr + ' Puffs')); + } + } else if (cat === 'respules') { + if (n === 1) { + add('1 Respule'); + add('1/2 Respule'); + add('2 Respules'); + add('1 Respule in 2.5 ml Saline'); + } else { + add(n === 1 ? '1 Respule' : (nStr + ' Respules')); + } + } else if (cat === 'spray') { + if (n === 1) { + add('1 Spray each nostril'); + add('2 Sprays each nostril'); + add('1 Spray'); + add('2 Sprays'); + } else { + add(nStr + ' Sprays each nostril'); + add(nStr + ' Sprays'); + } + } else if (cat === 'sachet') { + if (n === 1) { + add('1 Sachet in half glass water'); + add('1 Sachet'); + add('1/2 Sachet'); + add('1 Scoop'); + add('5 g'); + add('10 g'); + } else { + add(nStr + ' Sachets'); + add(nStr + ' g'); + } + } else if (cat === 'injection') { + if (n === 1) { + add('1 Amp'); + add('1 Vial'); + add('1 ml'); + add('10 ml'); + add('100 ml'); + add('100 mg'); + add('1 g'); + add('1000 IU'); + } else if (n === 2) { + add('2 ml'); + add('2 Amp'); + add('2 Vial'); + add('250 mg'); + add('2 g'); + } else if (n === 5) { + add('5 ml'); + add('500 mg'); + add('500 ml (IV)'); + add('5000 IU'); + } else { + add(nStr + ' ml'); + add(nStr + ' Amp'); + add(nStr + ' Vial'); + add(nStr + ' mg'); + } + } else if (cat === 'topical') { + if (n === 1) { + add('1 Application'); + add('1 Fingertip Unit (FTU)'); + add('1 Patch'); + add('Apply Thinly'); + } else { + add(nStr + ' Applications'); + add(nStr + ' Patches'); + } + } else if (cat === 'suppository') { + add(nStr + ' Suppository'); + add(nStr + ' Pessary'); + add(nStr + ' Enema'); + } else if (cat === 'gargle') { + add(nStr + '0 ml in half cup warm water'); + add(nStr + ' ml'); + } else { + if (n === 1) { + add('1 Tab'); + add('1/2 Tab'); + add('1 Cap'); + add('1 tsp (5 ml)'); + add('1 ml'); + add('1 Puff'); + add('1 Sachet'); + add('1 Drop'); + add('1 Amp'); + add('100 mg'); + add('500 mg'); + } else if (n === 2) { + add('2 Tab'); + add('2 Cap'); + add('2 tsp (10 ml)'); + add('2 ml'); + add('2.5 ml (1/2 tsp)'); + add('2 Puffs'); + add('2 Drops'); + add('250 mg'); + } else if (n === 5) { + add('5 ml (1 tsp)'); + add('500 mg'); + add('50 mg'); + add('5 mg'); + add('5 Drops'); + add('5 g'); + } else { + add(nStr + ' Tab'); + add(nStr + ' ml'); + add(nStr + ' mg'); + add(nStr + ' Cap'); + } + } + } + + var presets = _DOSAGE_PRESETS_BY_CAT[cat] || _DOSAGE_PRESETS_BY_CAT.general; + presets.forEach(function(p) { + if (!q || p.toLowerCase().indexOf(q) !== -1) { + add(p); + } + }); + + if (medicineDoseMasterCache && Array.isArray(medicineDoseMasterCache.dose)) { + medicineDoseMasterCache.dose.forEach(function(item) { + var label = (item.label || item.name || '').toString().trim(); + if (label) { + if (!q || label.toLowerCase().indexOf(q) !== -1) { + add(label); + } + } + }); + } + + return suggestions.slice(0, 12); + } + + function renderMedDosageDropdown(sugs) { + var $dd = $('#med_dosage_dd').empty(); + medDosageHighlightIdx = -1; + if (!sugs.length) { $dd.hide(); return; } + + sugs.forEach(function(s, idx) { + var $row = $('
') + .text(s); + + $row.on('mouseenter', function() { highlightMedDosageItem(idx); }) + .on('mouseleave', function() { $(this).css('background',''); }) + .on('mousedown', function(e) { e.preventDefault(); }) + .on('click', function() { + $('#med_dosage').val(s).trigger('change'); + $dd.hide().empty(); + medDosageHighlightIdx = -1; + setTimeout(function() { $('#med_freq').focus(); }, 30); + }); + + $dd.append($row); + }); + + $dd.show(); + } + + $('#med_dosage').on('input focus', function() { + var q = ($(this).val() || '').trim(); + var sugs = getMedDosageSuggestions(q); + renderMedDosageDropdown(sugs); + }).on('blur', function() { + setTimeout(function() { $('#med_dosage_dd').hide(); medDosageHighlightIdx = -1; }, 200); + }).on('keydown', function(e) { + var $dd = $('#med_dosage_dd'); + var isVisible = $dd.is(':visible'); + var $items = $('#med_dosage_dd .med-dosage-dd-item'); + + if (e.key === 'ArrowDown') { + if (!isVisible) { + var sugs = getMedDosageSuggestions(($(this).val() || '').trim()); + renderMedDosageDropdown(sugs); + highlightMedDosageItem(0); + return; + } + e.preventDefault(); + var nextIdx = medDosageHighlightIdx + 1; + if (nextIdx >= $items.length) nextIdx = 0; + highlightMedDosageItem(nextIdx); + } else if (e.key === 'ArrowUp') { + if (!isVisible) return; + e.preventDefault(); + var prevIdx = medDosageHighlightIdx - 1; + if (prevIdx < 0) prevIdx = $items.length - 1; + highlightMedDosageItem(prevIdx); + } else if (e.key === 'Enter') { + if (isVisible) { + e.preventDefault(); + var targetIdx = medDosageHighlightIdx >= 0 ? medDosageHighlightIdx : 0; + var $target = $items.eq(targetIdx); + if ($target.length) { + $target.trigger('click'); + } + } + } else if (e.key === 'Escape') { + $dd.hide().empty(); + medDosageHighlightIdx = -1; + } + }); + // ─── Medicine Frequency Smart Autocomplete ────────────────────────────── var _FREQ_PRESET_MAP = [ { code: 'OD', desc: 'Once Daily (1 dose/day)' }, @@ -9171,6 +9636,10 @@ if (!$('#med_where').val()) { autoSelectDefaultRoute($(this).val(), $('#med_where')); } + if ($('#med_dosage_dd').is(':visible') || $('#med_dosage').is(':focus')) { + var q = ($('#med_dosage').val() || '').trim(); + renderMedDosageDropdown(getMedDosageSuggestions(q)); + } }); function applyMedicineSelection(rowData) {