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('= base_url('Opd_prescription/fhir_bundle_submit') ?>', {
+ 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 || '{}') : '{}';