diff --git a/app/Config/Routes.php b/app/Config/Routes.php index a449ef3..17a181f 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_bundle_submit', 'Opd_prescription::fhir_bundle_submit'); $routes->post('Opd_prescription/fhir_complaint_recode', 'Opd_prescription::fhir_complaint_recode'); $routes->post('Opd_prescription/fhir_diagnosis_recode', 'Opd_prescription::fhir_diagnosis_recode'); $routes->get('Opd_prescription/complaints_search', 'Opd_prescription::complaints_search'); @@ -667,6 +668,7 @@ $routes->get('Opd_prescription/opd_medicince_data', 'Opd_prescription::opd_medic $routes->get('Opd_prescription/opd_medicince_export', 'Opd_prescription::opd_medicince_export'); $routes->get('Opd_prescription/opd_medicince_company_suggest', 'Opd_prescription::opd_medicince_company_suggest'); $routes->get('Opd_prescription/opd_medicince_get/(:num)', 'Opd_prescription::opd_medicince_get/$1'); +$routes->get('Opd_prescription/opd_medicince_snomed_lookup', 'Opd_prescription::opd_medicince_snomed_lookup'); $routes->post('Opd_prescription/opd_medicince_save', 'Opd_prescription::opd_medicince_save'); $routes->post('Opd_prescription/opd_medicince_ai_details', 'Opd_prescription::opd_medicince_ai_details'); $routes->post('Opd_prescription/opd_medicince_remove/(:num)', 'Opd_prescription::opd_medicince_remove/$1'); diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 8865e18..34e7480 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -2670,6 +2670,68 @@ class Opd_prescription extends BaseController return $this->response->setJSON(['update' => 1, 'row' => $row]); } + /** + * SNOMED CT substance lookup for the medicine master form. + * Searches CSNOtk (substance/product semantic tags) for the given drug name term. + * Returns a ranked list of SNOMED CT concepts to pick from. + * + * GET /Opd_prescription/opd_medicince_snomed_lookup?term=amoxicillin&limit=10 + */ + public function opd_medicince_snomed_lookup() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'rows' => []]); + } + + $term = trim((string) $this->request->getGet('term')); + $limit = max(1, min(20, (int) ($this->request->getGet('limit') ?: 10))); + + if ($term === '') { + return $this->response->setJSON(['update' => 1, 'rows' => [], 'message' => 'No term provided']); + } + + try { + $svc = new \App\Libraries\CsnotkTerminologyService(); + $rows = $svc->searchSubstance($term, $limit); + } catch (\Throwable $e) { + return $this->response->setJSON(['update' => 0, 'rows' => [], 'message' => 'SNOMED service error: ' . $e->getMessage()]); + } + + // Normalise each row: extract clean display name + substance/product hint + $out = []; + foreach ($rows as $r) { + $conceptId = trim((string) ($r['concept_id'] ?? '')); + $fsn = trim((string) ($r['fsn'] ?? '')); + $term_ = trim((string) ($r['term'] ?? $fsn)); + $hierarchy = trim((string) ($r['hierarchy'] ?? '')); + $source = trim((string) ($r['source'] ?? 'local')); + + if ($conceptId === '' || $term_ === '') { + continue; + } + + // Derive a clean INN-like name (term without parenthetical tag) + $displayTerm = $fsn !== '' ? preg_replace('/\s*\([^)]+\)\s*$/', '', $fsn) : $term_; + $displayTerm = trim((string) $displayTerm); + + $out[] = [ + 'concept_id' => $conceptId, + 'term' => $term_, + 'fsn' => $fsn, + 'display_term' => $displayTerm ?: $term_, + 'hierarchy' => $hierarchy, + 'source' => $source, + ]; + } + + return $this->response->setJSON([ + 'update' => 1, + 'rows' => $out, + 'count' => count($out), + 'message' => count($out) === 0 ? 'No SNOMED CT substance concepts found for "' . $term . '"' : '', + ]); + } + public function opd_medicince_save() { if (! $this->request->isAJAX()) { @@ -4658,26 +4720,199 @@ class Opd_prescription extends BaseController $bundleType = htmlspecialchars((string) ($row['bundle_type'] ?? ''), ENT_QUOTES, 'UTF-8'); $entryCount = isset($bundle['entry']) && is_array($bundle['entry']) ? count($bundle['entry']) : 0; + $csrfTokName = htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8'); + $csrfTokHash = htmlspecialchars(csrf_hash(), ENT_QUOTES, 'UTF-8'); + $submitUrl = base_url('Opd_prescription/fhir_bundle_submit'); + $html = '' . '' + . '' + . '' . 'FHIR Bundle Preview — OPD #' . $opdId . '' . '' - . '' + . '' + . '' . '
' - . '
' - . '
FHIR Bundle Preview
' - . '
' . $bundleType . '' - . '' . $entryCount . ' entries' - . 'Generated: ' . $generatedAt - . '  |  Doc #' . $docId . '  |  OPD #' . $opdId . ' / Session #' . $sessionId . '' + . '
' + . '
FHIR Bundle Preview — OPD #' . $opdId . '
' + . '
' + . '' . $bundleType . '' + . '' . $entryCount . ' entries' + . 'Generated: ' . $generatedAt . ' | Doc #' . $docId . '' + . '' . '
' + . '
' . '
'
-            . '
' + . '
' + . '' . ''; return $this->response->setHeader('Content-Type', 'text/html; charset=UTF-8')->setBody($html); } + public function fhir_bundle_submit() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'AJAX only']); + } + + $opdId = (int) $this->request->getPost('opd_id'); + $sessionId = (int) $this->request->getPost('opd_session_id'); + + if ($opdId <= 0) { + return $this->response->setJSON(['ok' => 0, 'error_text' => 'OPD ID required', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]); + } + + if (! $this->db->tableExists('opd_fhir_documents')) { + return $this->response->setJSON(['ok' => 0, 'error_text' => 'FHIR bundle storage not available', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]); + } + + $builder = $this->db->table('opd_fhir_documents') + ->where('opd_id', $opdId) + ->where('bundle_type', 'MedicationRequestBundle'); + if ($sessionId > 0) { + $builder->where('opd_session_id', $sessionId); + } + $bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray(); + + if (empty($bundleRow)) { + return $this->response->setJSON([ + 'ok' => 0, + 'error_text' => 'No FHIR bundle found. Save the prescription first.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + $bundleJson = (string) ($bundleRow['bundle_json'] ?? '{}'); + $bundle = json_decode($bundleJson, true); + if (! is_array($bundle)) { + return $this->response->setJSON([ + 'ok' => 0, + 'error_text' => 'Stored FHIR bundle JSON is invalid.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + // Gather patient / OPD context for the push payload + $patientId = 0; + $patientName = ''; + $abhaId = ''; + $doctorName = ''; + $visitDate = date('Y-m-d'); + + if ($this->db->tableExists('opd_master')) { + $opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray(); + if (! empty($opdRow)) { + $patientId = (int) ($opdRow['p_id'] ?? 0); + $rawDate = (string) ($opdRow['opd_date'] ?? ''); + if ($rawDate !== '') { + $ts = strtotime($rawDate); + if ($ts !== false) { + $visitDate = date('Y-m-d', $ts); + } + } + $docId2 = (int) ($opdRow['doc_id'] ?? 0); + if ($docId2 > 0 && $this->db->tableExists('doctor_master')) { + $dRow = $this->db->table('doctor_master')->where('id', $docId2)->get(1)->getRowArray(); + if (! empty($dRow)) { + $doctorName = trim((string) ($dRow['p_fname'] ?? '')); + } + } + } + } + + if ($patientId > 0 && $this->db->tableExists('patient_master')) { + $pFields = $this->db->getFieldNames('patient_master'); + $pRow = $this->db->table('patient_master')->where('p_id', $patientId)->get(1)->getRowArray(); + if (! empty($pRow)) { + $fname = trim((string) ($pRow['p_fname'] ?? '')); + $lname = trim((string) ($pRow['p_lname'] ?? '')); + $patientName = trim($fname . ' ' . $lname); + foreach (['abha_number', 'abha_id', 'abha_address'] as $abhaField) { + if (in_array($abhaField, $pFields, true)) { + $val = trim((string) ($pRow[$abhaField] ?? '')); + if ($val !== '') { + $abhaId = $val; + break; + } + } + } + } + } + + try { + $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + + $pushData = [ + 'patient_id' => (string) $patientId, + 'patient_name' => $patientName, + 'hi_type' => 'OPConsultRecord', + 'visit_date' => $visitDate, + 'care_context_reference' => 'OPD-' . $opdId, + 'record_data' => $bundle, + ]; + if ($abhaId !== '') { + $pushData['abha_address'] = $abhaId; + } + if ($doctorName !== '') { + $pushData['doctor_name'] = $doctorName; + } + + $result = $connector->pushRecord($pushData); + $ok = (int) ($result['ok'] ?? 0); + $queueId = (string) ($result['queue_id'] ?? $result['record_id'] ?? $result['id'] ?? ''); + $msg = (string) ($result['message'] ?? ($ok === 1 + ? 'FHIR bundle submitted to ABDM bridge successfully.' + : ($result['error_text'] ?? 'Submission failed. Check bridge configuration.'))); + + return $this->response->setJSON([ + 'ok' => $ok, + 'queue_id' => $queueId, + 'message' => $msg, + 'http_code' => (int) ($result['http_code'] ?? 0), + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } catch (\Throwable $e) { + log_message('error', '[fhir_bundle_submit] OPD#' . $opdId . ': ' . $e->getMessage()); + return $this->response->setJSON([ + 'ok' => 0, + 'message' => 'Bridge connection failed: ' . $e->getMessage(), + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + } + public function fhir_complaint_recode() { if (! $this->request->isAJAX()) { diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index 4f3e57c..aee6028 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -316,6 +316,7 @@
+ @@ -1600,6 +1601,41 @@ }); })(); + document.getElementById('btnSubmitFhirToAbdm').addEventListener('click', function () { + var btn = this; + if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; } + if (!confirm('Submit FHIR bundle for OPD #' + _fhirOpdId + ' to ABDM bridge?')) return; + var origHtml = btn.innerHTML; + btn.disabled = true; + btn.textContent = 'Submitting…'; + var body = new URLSearchParams({ opd_id: _fhirOpdId, opd_session_id: _fhirSessionId }); + body.append(csrfName, csrfHash); + fetch('', { + 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.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; } + if (res.ok == 1) { + btn.className = 'btn btn-sm btn-success'; + btn.innerHTML = ' Submitted ✓'; + var qInfo = res.queue_id ? ' Queue ID: ' + res.queue_id : ''; + alert('✅ Bundle submitted to ABDM bridge.\n' + (res.message || '') + qInfo); + } else { + btn.disabled = false; + btn.innerHTML = origHtml; + alert('❌ Submission failed:\n' + (res.message || res.error_text || 'Check bridge configuration.') + (res.http_code ? ' (HTTP ' + res.http_code + ')' : '')); + } + }) + .catch(function (e) { + btn.disabled = false; + btn.innerHTML = origHtml; + alert('❌ Error: ' + e.message); + }); + }); + document.getElementById('btnCopyFhirModal').addEventListener('click', function () { var btn = this; var jsonText = fhirModalJson ? (fhirModalJson.textContent || '{}') : '{}';