diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3bdc9cc..e5fbb36 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -590,6 +590,7 @@ $routes->get('Opd_prescription/fhir_bundle_history/(:num)', 'Opd_prescription::f $routes->get('Opd_prescription/fhir_bundle_history/(:num)/(:num)', 'Opd_prescription::fhir_bundle_history/$1/$2'); $routes->get('Opd_prescription/fhir_bundle_preview/(:num)', 'Opd_prescription::fhir_bundle_preview/$1'); $routes->get('Opd_prescription/fhir_bundle_preview/(:num)/(:num)', 'Opd_prescription::fhir_bundle_preview/$1/$2'); +$routes->post('Opd_prescription/fhir_complaint_recode', 'Opd_prescription::fhir_complaint_recode'); $routes->get('Opd_prescription/complaints_search', 'Opd_prescription::complaints_search'); $routes->get('Opd_prescription/complaints_extract', 'Opd_prescription::complaints_extract'); $routes->get('Opd_prescription/section_recent_entries', 'Opd_prescription::section_recent_entries'); diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index a1c3ac8..0e23672 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -4640,7 +4640,93 @@ class Opd_prescription extends BaseController return $this->response->setHeader('Content-Type', 'text/html; charset=UTF-8')->setBody($html); } - public function advice_search() + public function fhir_complaint_recode() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['update' => 0]); + } + + $opdId = (int) $this->request->getPost('opd_id'); + $sessionId = (int) $this->request->getPost('opd_session_id'); + $complaintText = trim((string) $this->request->getPost('complaint_text')); + $snomedId = trim((string) $this->request->getPost('snomed_concept_id')); + $snomedTerm = trim((string) $this->request->getPost('snomed_term')); + + if ($opdId <= 0 || $complaintText === '') { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Missing required fields']); + } + if (! $this->db->tableExists('opd_prescription')) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']); + } + + // Resolve the prescription row + $prescRow = null; + if ($sessionId > 0) { + $prescRow = $this->db->table('opd_prescription') + ->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRowArray(); + } + if (empty($prescRow)) { + $prescRow = $this->db->table('opd_prescription') + ->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRowArray(); + } + if (empty($prescRow)) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Prescription record not found']); + } + + $prescId = (int) $prescRow['id']; + $snomedJson = trim((string) ($prescRow['complaint_snomed_json'] ?? '')); + $items = []; + if ($snomedJson !== '' && $snomedJson !== '[]') { + $decoded = json_decode($snomedJson, true); + if (is_array($decoded)) { + $items = $decoded; + } + } + + // Bootstrap from free-text if no structured items yet + if (empty($items)) { + $parts = preg_split('/[,;\n\r]+/', (string) ($prescRow['complaints'] ?? '')) ?: []; + foreach ($parts as $part) { + $part = trim((string) $part); + if ($part !== '' && preg_match('/^Complaint\s*(Onset|Duration|Severity)\s*:/i', $part) !== 1) { + $items[] = ['term' => $part, 'concept_id' => '', 'source' => 'local', + 'hierarchy' => '', 'severity' => '', 'duration' => '', 'frequency' => '', 'date' => '']; + } + } + } + + // Find and update matching complaint (case-insensitive) + $found = false; + foreach ($items as &$item) { + if (strtolower(trim((string) ($item['term'] ?? ''))) === strtolower($complaintText)) { + $item['concept_id'] = $snomedId; + $item['term'] = $snomedTerm ?: $complaintText; + $item['source'] = $snomedId !== '' ? 'snomed' : ($item['source'] ?? 'local'); + $found = true; + break; + } + } + unset($item); + + if (! $found) { + $items[] = ['term' => $snomedTerm ?: $complaintText, 'concept_id' => $snomedId, + 'source' => $snomedId !== '' ? 'snomed' : 'local', + 'hierarchy' => '', 'severity' => '', 'duration' => '', 'frequency' => '', 'date' => '']; + } + + $newJson = (string) json_encode(array_values($items), JSON_UNESCAPED_UNICODE); + $this->db->table('opd_prescription')->where('id', $prescId)->update(['complaint_snomed_json' => $newJson]); + + return $this->response->setJSON([ + 'update' => 1, + 'error_text' => 'Saved', + 'presc_id' => $prescId, + 'snomed_concept_id'=> $snomedId, + 'snomed_term' => $snomedTerm, + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } { $q = trim((string) $this->request->getGet('q')); if (! $this->db->tableExists('opd_advice')) { diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index d84089b..49af2db 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -988,6 +988,134 @@ fhirFormView.style.display = 'none'; }); + // ── Track current bundle context for edits ────────────────────── + var _fhirOpdId = 0, _fhirSessionId = 0; + + // ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─ + var _editTimer = null; + fhirFormView.addEventListener('click', function(e) { + var editBtn = e.target.closest('.fhir-complaint-edit'); + var cancelBtn = e.target.closest('.fhir-complaint-cancel'); + var saveBtn = e.target.closest('.fhir-complaint-save'); + var ddItem = e.target.closest('.fhir-snomed-dd-item'); + + if (editBtn) { + var panel = fhirFormView.querySelector('.fhir-edit-panel[data-idx="' + editBtn.dataset.idx + '"]'); + if (!panel) return; + // Close others + fhirFormView.querySelectorAll('.fhir-edit-panel').forEach(function(p) { if (p !== panel) p.style.display = 'none'; }); + panel.style.display = panel.style.display === 'none' ? '' : 'none'; + } + if (cancelBtn) { + cancelBtn.closest('.fhir-edit-panel').style.display = 'none'; + } + if (ddItem) { + var panel2 = ddItem.closest('.fhir-edit-panel'); + panel2.querySelector('.fhir-snomed-id').value = ddItem.dataset.id || ''; + panel2.querySelector('.fhir-snomed-term').value = ddItem.dataset.term || ''; + panel2.querySelector('.fhir-snomed-search').value = ddItem.dataset.term || ''; + panel2.querySelector('.fhir-snomed-dd').style.display = 'none'; + var sel = panel2.querySelector('.fhir-snomed-selected'); + sel.textContent = '✓ ' + (ddItem.dataset.id || '') + ' — ' + (ddItem.dataset.term || ''); + sel.style.display = ''; + } + if (saveBtn) { + var panel3 = saveBtn.closest('.fhir-edit-panel'); + var sId = panel3.querySelector('.fhir-snomed-id').value.trim(); + var sTerm = panel3.querySelector('.fhir-snomed-term').value.trim(); + var cText = panel3.dataset.complaintText || ''; + if (!sId && !sTerm) { + // Try the search field text as the term if nothing selected + sTerm = panel3.querySelector('.fhir-snomed-search').value.trim(); + } + var csrfN = document.querySelector('meta[name="csrf-token-name"]'); + var csrfH = document.querySelector('meta[name="csrf-token-hash"]'); + var body = new URLSearchParams({ + opd_id: _fhirOpdId, + opd_session_id: _fhirSessionId, + complaint_text: cText, + snomed_concept_id: sId, + snomed_term: sTerm + }); + body.append(csrfName, csrfHash); + saveBtn.disabled = true; + saveBtn.textContent = 'Saving…'; + fetch('= base_url('Opd_prescription/fhir_complaint_recode') ?>', { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString() + }) + .then(function(r){ return r.json(); }) + .then(function(res){ + if (res.update == 1) { + // Update the CSRF token for next request + if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; } + // Update the displayed complaint badge inline + var li = panel3.closest('li'); + if (li && sId) { + var existing = li.querySelector('.fhir-snomed-badge'); + if (existing) { existing.textContent = 'SNOMED\u00a0' + sId; } + else { + var badge = document.createElement('span'); + badge.className = 'badge bg-info text-dark ms-1 fhir-snomed-badge'; + badge.style.fontSize = '.7rem'; + badge.textContent = 'SNOMED\u00a0' + sId; + var strong = li.querySelector('strong'); + if (strong) strong.insertAdjacentElement('afterend', badge); + } + } + panel3.style.display = 'none'; + // Refresh quality badges + } else { + alert(res.error_text || 'Save failed'); + } + }) + .catch(function(e){ alert('Error: ' + e.message); }) + .finally(function(){ saveBtn.disabled = false; saveBtn.textContent = 'Save'; }); + } + }); + fhirFormView.addEventListener('input', function(e) { + var searchInput = e.target.closest('.fhir-snomed-search'); + if (!searchInput) return; + var panel = searchInput.closest('.fhir-edit-panel'); + var dd = panel.querySelector('.fhir-snomed-dd'); + var q = searchInput.value.trim(); + if (q.length < 2) { dd.style.display = 'none'; return; } + clearTimeout(_editTimer); + _editTimer = setTimeout(function() { + fetch('= base_url('Opd_prescription/complaints_search') ?>?q=' + encodeURIComponent(q), { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function(r){ return r.json(); }) + .then(function(res){ + dd.innerHTML = ''; + var rows = (res.rows || res.results || []); + if (!rows.length) { dd.style.display = 'none'; return; } + rows.slice(0, 10).forEach(function(row) { + var id = row.concept_id || row.snomed_concept_id || ''; + var name = row.name || row.term || row.snomed_term || ''; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-group-item list-group-item-action py-1 px-2 fhir-snomed-dd-item'; + btn.dataset.id = id; + btn.dataset.term = name; + btn.innerHTML = '' + hesc(name) + (id ? ' ' + hesc(id) + '' : '') + ''; + dd.appendChild(btn); + }); + dd.style.display = ''; + }) + .catch(function(){ dd.style.display = 'none'; }); + }, 280); + }); + fhirFormView.addEventListener('focusout', function(e) { + if (!e.target.closest('.fhir-edit-panel')) return; + // Delay hiding so click on dd item registers first + setTimeout(function() { + var dd = e.target.closest('.fhir-edit-panel'); + if (dd) { var d = dd.querySelector('.fhir-snomed-dd'); if (d) d.style.display = 'none'; } + }, 200); + }); + // ── Render FHIR bundle as human-readable form ────────────────────── function hesc(s) { return String(s || '') @@ -1046,7 +1174,7 @@ if (complaints.length) { html += '
No FHIR bundle generated yet.
'; if (fhirModalMeta) { fhirModalMeta.textContent = res.ok ? 'Loaded (' + res.status + ')' : 'Error (' + res.status + ')'; fhirModalMeta.className = 'flex-grow-1 small ' + (res.ok ? 'text-success' : 'text-danger'); }