From 0558ac5a2ed84a5d9d705eb7f120a8b4477fb421 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Mon, 25 May 2026 13:57:56 +0530 Subject: [PATCH] Pharmacy: add ABDM drug terminology autocomplete + canonical mapping --- app/Config/Routes.php | 4 + app/Controllers/Medical.php | 235 ++++++++++++++++++ ...rugTerminologyFieldsToMedProductMaster.php | 86 +++++++ app/Libraries/Abdm/EAtriaBridgeConnector.php | 43 ++++ app/Views/medical/product_edit.php | 213 +++++++++++++++- 5 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 app/Database/Migrations/2026-05-25-000602_AddAbdmDrugTerminologyFieldsToMedProductMaster.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1bc4ed4..1bc78a9 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -85,10 +85,14 @@ $routes->get('Product_master/medicine_category_Sub', 'Medical::medicine_category $routes->post('Product_master/medicine_category_Update', 'Medical::medicine_category_update'); $routes->get('product_master/drug_master_list', 'Medical::drug_master_list'); $routes->post('product_master/Product_search', 'Medical::product_search'); +$routes->get('product_master/drug_terminology_autocomplete', 'Medical::drug_terminology_autocomplete'); +$routes->get('product_master/drug_terminology_detail', 'Medical::drug_terminology_detail'); $routes->get('product_master/Product_edit/(:num)', 'Medical::product_edit/$1'); $routes->post('product_master/product_master_update/(:num)', 'Medical::product_master_update/$1'); $routes->get('Product_master/drug_master_list', 'Medical::drug_master_list'); $routes->post('Product_master/Product_search', 'Medical::product_search'); +$routes->get('Product_master/drug_terminology_autocomplete', 'Medical::drug_terminology_autocomplete'); +$routes->get('Product_master/drug_terminology_detail', 'Medical::drug_terminology_detail'); $routes->get('Product_master/Product_edit/(:num)', 'Medical::product_edit/$1'); $routes->post('Product_master/product_master_update/(:num)', 'Medical::product_master_update/$1'); $routes->get('Medical/Purchase', 'Medical::purchase'); diff --git a/app/Controllers/Medical.php b/app/Controllers/Medical.php index 5ea0b44..4533ddb 100644 --- a/app/Controllers/Medical.php +++ b/app/Controllers/Medical.php @@ -5071,6 +5071,226 @@ class Medical extends BaseController ]); } + public function drug_terminology_autocomplete() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $q = trim((string) ($this->request->getGet('q') ?? $this->request->getPost('q') ?? '')); + $q = preg_replace('/[^A-Za-z0-9_ \-]/', '', $q); + if (mb_strlen($q) < 2) { + return $this->response->setJSON([ + 'ok' => 1, + 'suggestions' => [], + 'error_text' => '', + ]); + } + + $type = strtolower(trim((string) ($this->request->getGet('type') ?? $this->request->getPost('type') ?? 'generic'))); + $allowedTypes = ['generic', 'brand', 'product', 'substance']; + if (! in_array($type, $allowedTypes, true)) { + $type = 'generic'; + } + + $limit = (int) ($this->request->getGet('limit') ?? $this->request->getPost('limit') ?? 10); + if ($limit <= 0 || $limit > 50) { + $limit = 10; + } + + try { + $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + $result = $connector->drugsAutocomplete($q, $type, $limit); + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'ok' => 0, + 'suggestions' => [], + 'error_text' => $e->getMessage(), + ]); + } + + $items = $this->extractDrugResultList($result); + $suggestions = []; + foreach ($items as $row) { + if (! is_array($row)) { + continue; + } + + $label = $this->extractDrugText($row, ['label', 'name', 'display', 'term', 'title', 'drug_name']); + $identifier = $this->extractDrugText($row, ['identifier', 'id', 'code', 'concept_id', 'value']); + $itemType = strtolower($this->extractDrugText($row, ['type', 'entity_type', 'category'])); + if ($itemType === '') { + $itemType = $type; + } + + if ($label === '' && $identifier === '') { + continue; + } + + $suggestions[] = [ + 'label' => $label !== '' ? $label : $identifier, + 'identifier' => $identifier, + 'type' => $itemType, + ]; + } + + return $this->response->setJSON([ + 'ok' => (int) ($result['ok'] ?? 0), + 'suggestions' => $suggestions, + 'error_text' => (string) ($result['error_text'] ?? $result['message'] ?? ''), + ]); + } + + public function drug_terminology_detail() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $type = strtolower(trim((string) ($this->request->getGet('type') ?? $this->request->getPost('type') ?? 'generic'))); + $identifier = trim((string) ($this->request->getGet('identifier') ?? $this->request->getPost('identifier') ?? '')); + if ($identifier === '') { + return $this->response->setJSON([ + 'ok' => 0, + 'selected' => null, + 'error_text' => 'Identifier is required', + ]); + } + + try { + $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + $result = $connector->drugsDetail($type, $identifier); + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'ok' => 0, + 'selected' => null, + 'error_text' => $e->getMessage(), + ]); + } + + $record = $this->extractDrugResultObject($result); + $label = $this->extractDrugText($record, ['label', 'name', 'display', 'term', 'title', 'drug_name']); + $generic = $this->extractDrugText($record, ['generic_name', 'genericname', 'generic', 'salt', 'molecule']); + $hsnCode = $this->extractDrugText($record, ['hsn_code', 'hsn', 'hscode']); + $formulation = $this->extractDrugText($record, ['formulation', 'dosage_form', 'drug_form']); + $packing = $this->extractDrugText($record, ['packing', 'pack_size', 'package']); + $itemType = strtolower($this->extractDrugText($record, ['type', 'entity_type', 'category'])); + if ($itemType === '') { + $itemType = $type; + } + $canonicalId = $this->extractDrugText($record, ['identifier', 'id', 'code', 'concept_id', 'value']); + if ($canonicalId === '') { + $canonicalId = $identifier; + } + + $payloadJson = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (! is_string($payloadJson)) { + $payloadJson = '{}'; + } + + return $this->response->setJSON([ + 'ok' => (int) ($result['ok'] ?? 0), + 'selected' => [ + 'label' => $label, + 'generic_name' => $generic, + 'hsn_code' => $hsnCode, + 'formulation' => $formulation, + 'packing' => $packing, + 'type' => $itemType, + 'identifier' => $canonicalId, + 'payload_json' => $payloadJson, + 'synced_at' => date('Y-m-d H:i:s'), + ], + 'error_text' => (string) ($result['error_text'] ?? $result['message'] ?? ''), + ]); + } + + /** + * @param array $result + * @return array> + */ + private function extractDrugResultList(array $result): array + { + $candidate = $result['data'] ?? $result['result'] ?? $result; + if (! is_array($candidate)) { + return []; + } + + if (array_is_list($candidate)) { + return array_values(array_filter($candidate, static fn ($v) => is_array($v))); + } + + foreach (['items', 'suggestions', 'results', 'data', 'records'] as $key) { + if (isset($candidate[$key]) && is_array($candidate[$key]) && array_is_list($candidate[$key])) { + return array_values(array_filter($candidate[$key], static fn ($v) => is_array($v))); + } + } + + if ($this->extractDrugText($candidate, ['identifier', 'id', 'code', 'concept_id', 'value']) !== '') { + return [$candidate]; + } + + return []; + } + + /** + * @param array $result + * @return array + */ + private function extractDrugResultObject(array $result): array + { + $candidate = $result['data'] ?? $result['result'] ?? $result; + if (! is_array($candidate)) { + return []; + } + + if (array_is_list($candidate)) { + $first = $candidate[0] ?? []; + return is_array($first) ? $first : []; + } + + foreach (['item', 'result', 'record', 'data'] as $key) { + if (isset($candidate[$key]) && is_array($candidate[$key])) { + return $candidate[$key]; + } + } + + return $candidate; + } + + /** + * @param array $data + * @param array $keys + */ + private function extractDrugText(array $data, array $keys): string + { + $flat = []; + $walker = static function ($node) use (&$walker, &$flat): void { + if (! is_array($node)) { + return; + } + foreach ($node as $k => $v) { + if (is_array($v)) { + $walker($v); + continue; + } + if (is_scalar($v) || $v === null) { + $flat[strtolower((string) $k)] = trim((string) $v); + } + } + }; + $walker($data); + + foreach ($keys as $key) { + $val = trim((string) ($flat[strtolower($key)] ?? '')); + if ($val !== '') { + return $val; + } + } + + return ''; + } + public function product_edit($productId = 0) { if ($deny = $this->ensurePharmacyAccess()) { @@ -5149,6 +5369,15 @@ class Medical extends BaseController $coldStorage = trim((string) ($this->request->getPost('input_cold_storage') ?? '')); $companyId = (int) ($this->request->getPost('input_company_name') ?? 0); $relatedDrugId = (int) ($this->request->getPost('related_drug_id') ?? 0); + $abdmDrugType = trim((string) ($this->request->getPost('abdm_drug_type') ?? '')); + $abdmDrugIdentifier = trim((string) ($this->request->getPost('abdm_drug_identifier') ?? '')); + $abdmDrugDisplay = trim((string) ($this->request->getPost('abdm_drug_display') ?? '')); + $abdmDrugGeneric = trim((string) ($this->request->getPost('abdm_drug_generic') ?? '')); + $abdmDrugPayloadJson = trim((string) ($this->request->getPost('abdm_drug_payload_json') ?? '')); + $abdmDrugLastSyncedAt = trim((string) ($this->request->getPost('abdm_drug_last_synced_at') ?? '')); + if (strlen($abdmDrugPayloadJson) > 65000) { + $abdmDrugPayloadJson = substr($abdmDrugPayloadJson, 0, 65000); + } $errors = []; if ($itemName === '' || mb_strlen($itemName) < 3) { @@ -5193,6 +5422,12 @@ class Medical extends BaseController 'shelf_no' => $shelfNo, 'cold_storage' => $coldStorage, 'company_id' => $companyId, + 'abdm_drug_type' => $abdmDrugType, + 'abdm_drug_identifier' => $abdmDrugIdentifier, + 'abdm_drug_display' => $abdmDrugDisplay, + 'abdm_drug_generic' => $abdmDrugGeneric, + 'abdm_drug_payload_json' => $abdmDrugPayloadJson, + 'abdm_drug_last_synced_at' => $abdmDrugLastSyncedAt, 'ban_flag_id' => $flag('chk_ban_flag_id'), 'batch_applicable' => $flag('chk_batch_applicable'), 'is_continue' => $flag('chk_is_continue'), diff --git a/app/Database/Migrations/2026-05-25-000602_AddAbdmDrugTerminologyFieldsToMedProductMaster.php b/app/Database/Migrations/2026-05-25-000602_AddAbdmDrugTerminologyFieldsToMedProductMaster.php new file mode 100644 index 0000000..7c2a574 --- /dev/null +++ b/app/Database/Migrations/2026-05-25-000602_AddAbdmDrugTerminologyFieldsToMedProductMaster.php @@ -0,0 +1,86 @@ +db->tableExists('med_product_master')) { + return; + } + + $fields = $this->db->getFieldNames('med_product_master') ?? []; + $add = []; + + if (! in_array('abdm_drug_type', $fields, true)) { + $add['abdm_drug_type'] = ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true, 'default' => null]; + } + if (! in_array('abdm_drug_identifier', $fields, true)) { + $add['abdm_drug_identifier'] = ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true, 'default' => null]; + } + if (! in_array('abdm_drug_display', $fields, true)) { + $add['abdm_drug_display'] = ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true, 'default' => null]; + } + if (! in_array('abdm_drug_generic', $fields, true)) { + $add['abdm_drug_generic'] = ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true, 'default' => null]; + } + if (! in_array('abdm_drug_payload_json', $fields, true)) { + $add['abdm_drug_payload_json'] = ['type' => 'MEDIUMTEXT', 'null' => true]; + } + if (! in_array('abdm_drug_last_synced_at', $fields, true)) { + $add['abdm_drug_last_synced_at'] = ['type' => 'DATETIME', 'null' => true, 'default' => null]; + } + + if ($add !== []) { + $this->forge->addColumn('med_product_master', $add); + } + + // Optional index for fast traceability lookup. + $indexes = $this->db->query('SHOW INDEX FROM med_product_master')->getResultArray(); + $hasIdx = false; + foreach ($indexes as $idx) { + if (strcasecmp((string) ($idx['Key_name'] ?? ''), 'idx_abdm_drug_identifier') === 0) { + $hasIdx = true; + break; + } + } + + if (! $hasIdx && in_array('abdm_drug_identifier', $this->db->getFieldNames('med_product_master') ?? [], true)) { + try { + $this->db->query('ALTER TABLE med_product_master ADD INDEX idx_abdm_drug_identifier (abdm_drug_identifier)'); + } catch (\Throwable $e) { + // Ignore index creation failures to keep migration non-blocking. + } + } + } + + public function down() + { + if (! $this->db->tableExists('med_product_master')) { + return; + } + + try { + $this->db->query('ALTER TABLE med_product_master DROP INDEX idx_abdm_drug_identifier'); + } catch (\Throwable $e) { + // no-op + } + + $fields = $this->db->getFieldNames('med_product_master') ?? []; + foreach ([ + 'abdm_drug_type', + 'abdm_drug_identifier', + 'abdm_drug_display', + 'abdm_drug_generic', + 'abdm_drug_payload_json', + 'abdm_drug_last_synced_at', + ] as $column) { + if (in_array($column, $fields, true)) { + $this->forge->dropColumn('med_product_master', $column); + } + } + } +} diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 33d9f83..29ce4ed 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -539,4 +539,47 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface { return $this->post('/v3/hip/link/sms-notify', $payload); } + + // ------------------------------------------------------------------------- + // Drug Terminology (CDCI) + // ------------------------------------------------------------------------- + + public function drugsAutocomplete(string $q, string $type = 'generic', int $limit = 10): array + { + $query = [ + 'q' => $q, + 'type' => $type, + 'limit' => max(1, min(50, $limit)), + ]; + + if ($this->hfrId !== '') { + $query['hfr_id'] = $this->hfrId; + } + + return $this->get('/v3/drugs/autocomplete', $query); + } + + public function drugsDetail(string $type, string $identifier): array + { + $query = [ + 'type' => $type, + 'identifier' => $identifier, + ]; + + if ($this->hfrId !== '') { + $query['hfr_id'] = $this->hfrId; + } + + return $this->get('/v3/drugs/detail', $query); + } + + public function drugsVersion(): array + { + $query = []; + if ($this->hfrId !== '') { + $query['hfr_id'] = $this->hfrId; + } + + return $this->get('/v3/drugs/version', $query); + } } diff --git a/app/Views/medical/product_edit.php b/app/Views/medical/product_edit.php index ca0732a..28891b1 100644 --- a/app/Views/medical/product_edit.php +++ b/app/Views/medical/product_edit.php @@ -16,6 +16,12 @@ $shelfNo = (string) ($product->shelf_no ?? ''); $coldStorage = (string) ($product->cold_storage ?? ''); $companyId = (int) ($product->company_id ?? 0); $relatedDrugId = (int) ($product->related_drug_id ?? 0); +$abdmDrugType = (string) ($product->abdm_drug_type ?? ''); +$abdmDrugIdentifier = (string) ($product->abdm_drug_identifier ?? ''); +$abdmDrugDisplay = (string) ($product->abdm_drug_display ?? ''); +$abdmDrugGeneric = (string) ($product->abdm_drug_generic ?? ''); +$abdmDrugPayloadJson = (string) ($product->abdm_drug_payload_json ?? ''); +$abdmDrugLastSyncedAt = (string) ($product->abdm_drug_last_synced_at ?? ''); $flag = static function ($v): string { return ((int) $v === 1) ? 'checked' : ''; @@ -25,16 +31,47 @@ $flag = static function ($v): string {
Product 0 ? '#' . $productId . '' : '' ?>
+
+ + + + + +
- +
+ +
+
+
+ ABDM Drug Search + +
+
@@ -131,6 +168,9 @@ $flag = static function ($v): string {